felhasználói felületre pü

This commit is contained in:
Roo
2026-07-27 08:39:18 +00:00
parent c9118bf52f
commit 6f28d3e70d
72 changed files with 6311 additions and 749 deletions

View File

@@ -2205,7 +2205,7 @@ const _FMnHxsRb1XnhSkgrcxwDEP1elfSPjrP7Wr27yXkb3Aw = (function(nitro) {
* Released under the MIT License.
*/
const _create = Object.create;
const create$2 = (obj = null) => _create(obj);
const create = (obj = null) => _create(obj);
/* eslint-enable */
/**
* Useful Utilities By Evan you
@@ -2240,7 +2240,7 @@ function deepCopy(src, des) {
// if src[key] is an object/array, set des[key]
// to empty object/array to prevent setting by reference
if (isObject(src[key]) && !isObject(des[key])) {
des[key] = Array.isArray(src[key]) ? [] : create$2();
des[key] = Array.isArray(src[key]) ? [] : create();
}
if (isNotObjectOrIsArray(des[key]) || isNotObjectOrIsArray(src[key])) {
// replace with src[key] when:
@@ -2259,18 +2259,18 @@ function deepCopy(src, des) {
const __nuxtMock = { runWithContext: async (fn) => await fn() };
const merger = createDefu((obj, key, value) => {
if (key === "messages" || key === "datetimeFormats" || key === "numberFormats") {
obj[key] ??= create$2(null);
obj[key] ??= create(null);
deepCopy(value, obj[key]);
return true;
}
});
async function loadVueI18nOptions(vueI18nConfigs) {
const nuxtApp = __nuxtMock;
let vueI18nOptions = { messages: create$2(null) };
let vueI18nOptions = { messages: create(null) };
for (const configFile of vueI18nConfigs) {
const resolver = await configFile().then((x) => isModule(x) ? x.default : x);
const resolved = isFunction(resolver) ? await nuxtApp.runWithContext(() => resolver()) : resolver;
vueI18nOptions = merger(create$2(null), resolved, vueI18nOptions);
vueI18nOptions = merger(create(null), resolved, vueI18nOptions);
}
vueI18nOptions.fallbackLocale ??= false;
return vueI18nOptions;
@@ -2416,6 +2416,7 @@ var menu$1 = {
finance_management: "Pénzügyek Kezelése",
packages: "Csomagok",
commission_rules: "Jutalék Szabályok",
financial_ledger: "Pénzügyi Napló",
gamification: "Gamification",
game_settings: "Játékmenet Beállítások",
point_rules: "Pontszabályok",
@@ -2429,6 +2430,7 @@ var menu$1 = {
statistics: "Statisztikák",
leaderboard: "Ranglista",
points_ledger: "Pontnapló",
ledger: "Pontnapló",
gamification_hq: "Gamification HQ",
system_config: "Rendszer Konfig",
system_parameters: "Rendszerparaméterek",
@@ -2441,16 +2443,31 @@ const locale_menu_46json_d8dcbc3e = {
};
var dashboard$1 = {
title: "Gamification HQ",
subtitle: "Gamification rendszer áttekintő irányítópultja",
title: "Admin Dashboard",
subtitle: "Rendszer áttekintő irányítópult",
loading: "Dashboard betöltése...",
error: "Nem sikerült betölteni a dashboard adatokat.",
active_users: "Aktív Felhasználók",
new_this_month: "új e hónapban",
total_vehicles: "Összes Jármű",
organizations: "Szervezetek",
memberships: "tagság",
new_users: "Új Felhasználók",
new_today: "ma",
users_by_role: "Felhasználók Szerepkör Szerint",
users_by_plan: "Felhasználók Csomag Szerint",
users_by_language: "Felhasználók Nyelv Szerint",
registration_trend: "Regisztrációs Trend",
quick_actions: "Gyors Műveletek",
invite_admin: "Admin Meghívása",
add_vehicle: "Jármű Hozzáadása",
view_reports: "Jelentések Megtekintése",
recent_activity: "Legutóbbi Tevékenység",
total_users: "Összes Felhasználó",
active_season: "Aktív Szezon",
pending_contributions: "Függő Hozzájárulások",
xp_earned_today: "Ma Szerzett XP",
none: "Nincs",
quick_actions: "Gyors Műveletek",
manage_point_rules: "Pontszabályok kezelése",
manage_badges: "Kitüntetések kezelése",
manage_seasons: "Szezonok kezelése",
@@ -3495,6 +3512,11 @@ const locale_gamification_46json_15470408 = {
};
var system$1 = {
logs: {
subtitle: "Rendszernaplók — auditált események áttekintése",
placeholder_title: "Rendszernaplók megtekintése",
placeholder_desc: "Itt jelennek meg a rendszer által auditált események, naplóbejegyzések és rendszerszintű műveletek. A modul még fejlesztés alatt áll."
},
config: {
title: "Rendszer Konfig",
subtitle: "Gamification master konfiguráció szerkesztése",
@@ -3519,7 +3541,18 @@ var system$1 = {
updated: "Konfiguráció frissítve!",
save_error: "Hiba történt a mentés során.",
json_applied: "JSON alkalmazva a form mezőkre!",
invalid_json: "Érvénytelen JSON: "
invalid_json: "Érvénytelen JSON: ",
inactivity_monitor: "Inaktivitás Figyelő",
inactivity_desc: "Az inaktivitási küszöbérték konfigurálása automatikus fiók felfüggesztéshez. Az ennyi napja inaktív felhasználók jelölésre kerülnek az MLM Jutalék Motor által.",
inactivity_threshold: "Inaktivitási Küszöb (nap)",
inactivity_range: "Tartomány: 30730 nap (alapértelmezett: 180)",
inactivity_updated: "Inaktivitási küszöb elmentve!",
inactivity_save_error: "Nem sikerült elmenteni az inaktivitási küszöböt.",
effective_threshold: "Hatályos Küszöb",
last_changed: "Utolsó módosítás",
never: "soha",
save_inactivity: "Mentés",
has_changes: "Változások észlelve"
},
params: {
title: "Rendszerparaméterek",
@@ -3778,6 +3811,82 @@ const locale_commission_46json_2229a18b = {
commission_rules: commission_rules$1
};
var financial_ledger$1 = {
title: "Pénzügyi Napló",
subtitle: "A teljes pénzügyi főkönyv böngészése — tranzakciók, felhasználók és részletek",
loading: "Napló betöltése...",
error: "Nem sikerült betölteni a pénzügyi naplót.",
retry: "Újrapróbálkozás",
no_results: "Nincsenek pénzügyi napló bejegyzések.",
no_results_hint: "Próbálj más szűrési feltételeket használni.",
id: "ID",
user: "Felhasználó",
user_id: "User ID",
amount: "Összeg",
currency: "Pénznem",
transaction_type: "Tranzakció Típus",
entry_type: "Típus",
wallet_type: "Tárca",
status: "Státusz",
created_at: "Dátum",
details: "Részletek",
filter: "Szűrés",
clear: "Törlés",
prev: "Előző",
next: "Következő",
page_info: "{page} / {total} oldal ({count} elem)",
export_csv: "CSV Export",
all_types: "Minden típus",
all_wallets: "Minden tárca",
all_statuses: "Minden státusz",
date_from: "Dátumtól",
date_to: "Dátumig",
search: "Keresés",
csv_downloaded: "CSV letöltés elindítva."
};
var enums$1 = {
entry_type: {
credit: "Jóváírás",
debit: "Terhelés",
CREDIT: "Jóváírás",
DEBIT: "Terhelés"
},
wallet_type: {
earned: "Megszerzett",
purchased: "Vásárolt",
service_coins: "Szolgáltatási Coin",
voucher: "Utalvány",
EARNED: "Megszerzett",
PURCHASED: "Vásárolt",
SERVICE_COINS: "Szolgáltatási Coin",
VOUCHER: "Utalvány"
},
status: {
pending: "Függőben",
completed: "Teljesítve",
success: "Sikeres",
failed: "Sikertelen",
cancelled: "Visszavonva",
refunded: "Visszatérítve",
refund: "Visszatérítés"
},
transaction_type: {
subscription: "Előfizetés",
commission: "Jutalék",
refund: "Visszatérítés",
manual_adjustment: "Kézi Korrekció",
purchase: "Vásárlás",
withdrawal: "Kivét",
bonus: "Bónusz",
fee: "Díj",
reward: "Jutalom"
}
};
const locale_finance_46json_ae59f62d = {
financial_ledger: financial_ledger$1,
enums: enums$1
};
var common$5 = {
loading: "Loading...",
saving: "Saving...",
@@ -3897,6 +4006,7 @@ var menu = {
finance_management: "Finance Management",
packages: "Packages",
commission_rules: "Commission Rules",
financial_ledger: "Financial Ledger",
gamification: "Gamification",
game_settings: "Game Settings",
point_rules: "Point Rules",
@@ -3910,6 +4020,7 @@ var menu = {
statistics: "Statistics",
leaderboard: "Leaderboard",
points_ledger: "Points Ledger",
ledger: "Points Ledger",
gamification_hq: "Gamification HQ",
system_config: "System Config",
system_parameters: "System Parameters",
@@ -4976,6 +5087,11 @@ const locale_gamification_46json_111ebf57 = {
};
var system = {
logs: {
subtitle: "System Logs — audited event overview",
placeholder_title: "System Logs Viewer",
placeholder_desc: "This section will display audited system events, log entries, and system-wide operations. Module is under development."
},
config: {
title: "System Config",
subtitle: "Gamification master configuration editor",
@@ -5000,7 +5116,18 @@ var system = {
updated: "Configuration updated!",
save_error: "Failed to save configuration.",
json_applied: "JSON applied to form fields!",
invalid_json: "Invalid JSON: "
invalid_json: "Invalid JSON: ",
inactivity_monitor: "Inactivity Monitor",
inactivity_desc: "Configure the inactivity threshold for automatic account suspension. Users inactive for this many days will be flagged by the MLM Commission Engine.",
inactivity_threshold: "Inactivity Threshold (days)",
inactivity_range: "Range: 30730 days (default: 180)",
inactivity_updated: "Inactivity threshold saved!",
inactivity_save_error: "Failed to save inactivity threshold.",
effective_threshold: "Effective Threshold",
last_changed: "Last changed",
never: "never",
save_inactivity: "Save",
has_changes: "Changes detected"
},
params: {
title: "System Parameters",
@@ -5259,6 +5386,82 @@ const locale_commission_46json_ea60861a = {
commission_rules: commission_rules
};
var financial_ledger = {
title: "Financial Ledger",
subtitle: "Browse the complete financial ledger — transactions, users and details",
loading: "Loading ledger...",
error: "Failed to load financial ledger.",
retry: "Retry",
no_results: "No financial ledger entries found.",
no_results_hint: "Try different filter criteria.",
id: "ID",
user: "User",
user_id: "User ID",
amount: "Amount",
currency: "Currency",
transaction_type: "Transaction Type",
entry_type: "Type",
wallet_type: "Wallet",
status: "Status",
created_at: "Date",
details: "Details",
filter: "Filter",
clear: "Clear",
prev: "Previous",
next: "Next",
page_info: "{page} / {total} pages ({count} items)",
export_csv: "Export CSV",
all_types: "All Types",
all_wallets: "All Wallets",
all_statuses: "All Statuses",
date_from: "Date From",
date_to: "Date To",
search: "Search",
csv_downloaded: "CSV download started."
};
var enums = {
entry_type: {
credit: "Credit",
debit: "Debit",
CREDIT: "Credit",
DEBIT: "Debit"
},
wallet_type: {
earned: "Earned",
purchased: "Purchased",
service_coins: "Service Coins",
voucher: "Voucher",
EARNED: "Earned",
PURCHASED: "Purchased",
SERVICE_COINS: "Service Coins",
VOUCHER: "Voucher"
},
status: {
pending: "Pending",
completed: "Completed",
success: "Success",
failed: "Failed",
cancelled: "Cancelled",
refunded: "Refunded",
refund: "Refund"
},
transaction_type: {
subscription: "Subscription",
commission: "Commission",
refund: "Refund",
manual_adjustment: "Manual Adjustment",
purchase: "Purchase",
withdrawal: "Withdrawal",
bonus: "Bonus",
fee: "Fee",
reward: "Reward"
}
};
const locale_finance_46json_5597e160 = {
financial_ledger: financial_ledger,
enums: enums
};
var common$4 = {
loading: "Wird geladen...",
saving: "Wird gespeichert...",
@@ -5775,6 +5978,11 @@ const localeLoaders = {
key: "locale_commission_46json_2229a18b",
load: () => Promise.resolve(locale_commission_46json_2229a18b),
cache: true
},
{
key: "locale_finance_46json_ae59f62d",
load: () => Promise.resolve(locale_finance_46json_ae59f62d),
cache: true
}
],
en: [
@@ -5822,6 +6030,11 @@ const localeLoaders = {
key: "locale_commission_46json_ea60861a",
load: () => Promise.resolve(locale_commission_46json_ea60861a),
cache: true
},
{
key: "locale_finance_46json_5597e160",
load: () => Promise.resolve(locale_finance_46json_5597e160),
cache: true
}
],
de: [
@@ -6512,22 +6725,7 @@ _1xyvuM2CQDhMlDxE8qY1t3Wi66ocgNUNDNce4pREOrU,
_wH6JrtIxmaSoA8lCPWFnE9z4lQeXW6H5z3l5aymEQw
];
const assets = {
"/index.mjs": {
"type": "text/javascript; charset=utf-8",
"etag": "\"3db35-gliVpBtK5107QX2IT8SR1tGeiAs\"",
"mtime": "2026-07-24T12:30:16.660Z",
"size": 252725,
"path": "index.mjs"
},
"/index.mjs.map": {
"type": "application/json",
"etag": "\"84c6f-gMC45UcEAiPwtJNbdMr3pGk5iUE\"",
"mtime": "2026-07-24T12:30:16.660Z",
"size": 543855,
"path": "index.mjs.map"
}
};
const assets = {};
function readAsset (id) {
const serverDir = dirname$1(fileURLToPath(globalThis._importMeta_.url));

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"id":"dev","timestamp":1784853723611}
{"id":"dev","timestamp":1785068325212}

View File

@@ -1 +1 @@
{"id":"dev","timestamp":1784853723611,"prerendered":[]}
{"id":"dev","timestamp":1785068325212,"prerendered":[]}

View File

@@ -1,5 +1,5 @@
{
"date": "2026-07-24T12:30:18.036Z",
"date": "2026-07-26T12:18:53.544Z",
"preset": "nitro-dev",
"framework": {
"name": "nuxt",
@@ -11,7 +11,7 @@
"dev": {
"pid": 19,
"workerAddress": {
"socketPath": "\u0000nitro-worker-19-30-30-3992.sock"
"socketPath": "\u0000nitro-worker-19-1-1-8775.sock"
}
}
}

View File

@@ -1,8 +1,8 @@
/// <reference types="@nuxtjs/i18n" />
/// <reference types="@nuxtjs/tailwindcss" />
/// <reference types="@pinia/nuxt" />
/// <reference types="@nuxt/telemetry" />
/// <reference types="@nuxtjs/i18n" />
/// <reference types="@nuxt/devtools" />
/// <reference types="@nuxtjs/tailwindcss" />
/// <reference types="@nuxt/telemetry" />
/// <reference path="types/nitro-layouts.d.ts" />
/// <reference path="types/builder-env.d.ts" />
/// <reference types="nuxt" />

View File

@@ -1,4 +1,4 @@
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 7/24/2026, 12:30:17 PM
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 7/26/2026, 12:18:45 PM
import "@nuxtjs/tailwindcss/config-ctx"
import configMerger from "@nuxtjs/tailwindcss/merger";

View File

@@ -1,6 +1,9 @@
{
"compilerOptions": {
"paths": {
"nuxt": [
"../node_modules/nuxt"
],
"@unhead/vue": [
"../node_modules/@unhead/vue"
],
@@ -16,9 +19,6 @@
"@nuxt/schema": [
"../node_modules/@nuxt/schema"
],
"nuxt": [
"../node_modules/nuxt"
],
"vue-i18n": [
"../node_modules/vue-i18n/dist/vue-i18n",
"../node_modules/vue-i18n"
@@ -47,9 +47,6 @@
"../node_modules/@intlify/message-compiler/dist/message-compiler",
"../node_modules/@intlify/message-compiler"
],
"nitropack": [
"../node_modules/nitropack"
],
"defu": [
"../node_modules/defu"
],
@@ -68,6 +65,9 @@
"unplugin-vue-router/client": [
"../node_modules/unplugin-vue-router/client"
],
"nitropack": [
"../node_modules/nitropack"
],
"vite/client": [
"../node_modules/vite/client"
],

View File

@@ -29,6 +29,9 @@
"@@/*": [
"../*"
],
"nuxt": [
"../node_modules/nuxt"
],
"@unhead/vue": [
"../node_modules/@unhead/vue"
],
@@ -44,9 +47,6 @@
"@nuxt/schema": [
"../node_modules/@nuxt/schema"
],
"nuxt": [
"../node_modules/nuxt"
],
"vue-i18n": [
"../node_modules/vue-i18n"
],
@@ -68,9 +68,6 @@
"@intlify/message-compiler": [
"../node_modules/@intlify/message-compiler"
],
"nitropack": [
"../node_modules/nitropack"
],
"defu": [
"../node_modules/defu"
],
@@ -89,6 +86,9 @@
"unplugin-vue-router/client": [
"../node_modules/unplugin-vue-router/client"
],
"nitropack": [
"../node_modules/nitropack"
],
"vite/client": [
"../node_modules/vite/client"
],

View File

@@ -0,0 +1,73 @@
{
"financial_ledger": {
"title": "Financial Ledger",
"subtitle": "Browse the complete financial ledger — transactions, users and details",
"loading": "Loading ledger...",
"error": "Failed to load financial ledger.",
"retry": "Retry",
"no_results": "No financial ledger entries found.",
"no_results_hint": "Try different filter criteria.",
"id": "ID",
"user": "User",
"user_id": "User ID",
"amount": "Amount",
"currency": "Currency",
"transaction_type": "Transaction Type",
"entry_type": "Type",
"wallet_type": "Wallet",
"status": "Status",
"created_at": "Date",
"details": "Details",
"filter": "Filter",
"clear": "Clear",
"prev": "Previous",
"next": "Next",
"page_info": "{page} / {total} pages ({count} items)",
"export_csv": "Export CSV",
"all_types": "All Types",
"all_wallets": "All Wallets",
"all_statuses": "All Statuses",
"date_from": "Date From",
"date_to": "Date To",
"search": "Search",
"csv_downloaded": "CSV download started."
},
"enums": {
"entry_type": {
"credit": "Credit",
"debit": "Debit",
"CREDIT": "Credit",
"DEBIT": "Debit"
},
"wallet_type": {
"earned": "Earned",
"purchased": "Purchased",
"service_coins": "Service Coins",
"voucher": "Voucher",
"EARNED": "Earned",
"PURCHASED": "Purchased",
"SERVICE_COINS": "Service Coins",
"VOUCHER": "Voucher"
},
"status": {
"pending": "Pending",
"completed": "Completed",
"success": "Success",
"failed": "Failed",
"cancelled": "Cancelled",
"refunded": "Refunded",
"refund": "Refund"
},
"transaction_type": {
"subscription": "Subscription",
"commission": "Commission",
"refund": "Refund",
"manual_adjustment": "Manual Adjustment",
"purchase": "Purchase",
"withdrawal": "Withdrawal",
"bonus": "Bonus",
"fee": "Fee",
"reward": "Reward"
}
}
}

View File

@@ -16,6 +16,7 @@
"finance_management": "Finance Management",
"packages": "Packages",
"commission_rules": "Commission Rules",
"financial_ledger": "Financial Ledger",
"gamification": "Gamification",
"game_settings": "Game Settings",
"point_rules": "Point Rules",
@@ -29,6 +30,7 @@
"statistics": "Statistics",
"leaderboard": "Leaderboard",
"points_ledger": "Points Ledger",
"ledger": "Points Ledger",
"gamification_hq": "Gamification HQ",
"system_config": "System Config",
"system_parameters": "System Parameters",

View File

@@ -1,5 +1,10 @@
{
"system": {
"logs": {
"subtitle": "System Logs — audited event overview",
"placeholder_title": "System Logs Viewer",
"placeholder_desc": "This section will display audited system events, log entries, and system-wide operations. Module is under development."
},
"config": {
"title": "System Config",
"subtitle": "Gamification master configuration editor",
@@ -24,7 +29,18 @@
"updated": "Configuration updated!",
"save_error": "Failed to save configuration.",
"json_applied": "JSON applied to form fields!",
"invalid_json": "Invalid JSON: "
"invalid_json": "Invalid JSON: ",
"inactivity_monitor": "Inactivity Monitor",
"inactivity_desc": "Configure the inactivity threshold for automatic account suspension. Users inactive for this many days will be flagged by the MLM Commission Engine.",
"inactivity_threshold": "Inactivity Threshold (days)",
"inactivity_range": "Range: 30730 days (default: 180)",
"inactivity_updated": "Inactivity threshold saved!",
"inactivity_save_error": "Failed to save inactivity threshold.",
"effective_threshold": "Effective Threshold",
"last_changed": "Last changed",
"never": "never",
"save_inactivity": "Save",
"has_changes": "Changes detected"
},
"params": {
"title": "System Parameters",

View File

@@ -1,15 +1,30 @@
{
"dashboard": {
"title": "Gamification HQ",
"subtitle": "Gamification rendszer áttekintő irányítópultja",
"title": "Admin Dashboard",
"subtitle": "Rendszer áttekintő irányítópult",
"loading": "Dashboard betöltése...",
"error": "Nem sikerült betölteni a dashboard adatokat.",
"active_users": "Aktív Felhasználók",
"new_this_month": "új e hónapban",
"total_vehicles": "Összes Jármű",
"organizations": "Szervezetek",
"memberships": "tagság",
"new_users": "Új Felhasználók",
"new_today": "ma",
"users_by_role": "Felhasználók Szerepkör Szerint",
"users_by_plan": "Felhasználók Csomag Szerint",
"users_by_language": "Felhasználók Nyelv Szerint",
"registration_trend": "Regisztrációs Trend",
"quick_actions": "Gyors Műveletek",
"invite_admin": "Admin Meghívása",
"add_vehicle": "Jármű Hozzáadása",
"view_reports": "Jelentések Megtekintése",
"recent_activity": "Legutóbbi Tevékenység",
"total_users": "Összes Felhasználó",
"active_season": "Aktív Szezon",
"pending_contributions": "Függő Hozzájárulások",
"xp_earned_today": "Ma Szerzett XP",
"none": "Nincs",
"quick_actions": "Gyors Műveletek",
"manage_point_rules": "Pontszabályok kezelése",
"manage_badges": "Kitüntetések kezelése",
"manage_seasons": "Szezonok kezelése",

View File

@@ -0,0 +1,73 @@
{
"financial_ledger": {
"title": "Pénzügyi Napló",
"subtitle": "A teljes pénzügyi főkönyv böngészése — tranzakciók, felhasználók és részletek",
"loading": "Napló betöltése...",
"error": "Nem sikerült betölteni a pénzügyi naplót.",
"retry": "Újrapróbálkozás",
"no_results": "Nincsenek pénzügyi napló bejegyzések.",
"no_results_hint": "Próbálj más szűrési feltételeket használni.",
"id": "ID",
"user": "Felhasználó",
"user_id": "User ID",
"amount": "Összeg",
"currency": "Pénznem",
"transaction_type": "Tranzakció Típus",
"entry_type": "Típus",
"wallet_type": "Tárca",
"status": "Státusz",
"created_at": "Dátum",
"details": "Részletek",
"filter": "Szűrés",
"clear": "Törlés",
"prev": "Előző",
"next": "Következő",
"page_info": "{page} / {total} oldal ({count} elem)",
"export_csv": "CSV Export",
"all_types": "Minden típus",
"all_wallets": "Minden tárca",
"all_statuses": "Minden státusz",
"date_from": "Dátumtól",
"date_to": "Dátumig",
"search": "Keresés",
"csv_downloaded": "CSV letöltés elindítva."
},
"enums": {
"entry_type": {
"credit": "Jóváírás",
"debit": "Terhelés",
"CREDIT": "Jóváírás",
"DEBIT": "Terhelés"
},
"wallet_type": {
"earned": "Megszerzett",
"purchased": "Vásárolt",
"service_coins": "Szolgáltatási Coin",
"voucher": "Utalvány",
"EARNED": "Megszerzett",
"PURCHASED": "Vásárolt",
"SERVICE_COINS": "Szolgáltatási Coin",
"VOUCHER": "Utalvány"
},
"status": {
"pending": "Függőben",
"completed": "Teljesítve",
"success": "Sikeres",
"failed": "Sikertelen",
"cancelled": "Visszavonva",
"refunded": "Visszatérítve",
"refund": "Visszatérítés"
},
"transaction_type": {
"subscription": "Előfizetés",
"commission": "Jutalék",
"refund": "Visszatérítés",
"manual_adjustment": "Kézi Korrekció",
"purchase": "Vásárlás",
"withdrawal": "Kivét",
"bonus": "Bónusz",
"fee": "Díj",
"reward": "Jutalom"
}
}
}

View File

@@ -16,6 +16,7 @@
"finance_management": "Pénzügyek Kezelése",
"packages": "Csomagok",
"commission_rules": "Jutalék Szabályok",
"financial_ledger": "Pénzügyi Napló",
"gamification": "Gamification",
"game_settings": "Játékmenet Beállítások",
"point_rules": "Pontszabályok",
@@ -29,6 +30,7 @@
"statistics": "Statisztikák",
"leaderboard": "Ranglista",
"points_ledger": "Pontnapló",
"ledger": "Pontnapló",
"gamification_hq": "Gamification HQ",
"system_config": "Rendszer Konfig",
"system_parameters": "Rendszerparaméterek",

View File

@@ -1,5 +1,10 @@
{
"system": {
"logs": {
"subtitle": "Rendszernaplók — auditált események áttekintése",
"placeholder_title": "Rendszernaplók megtekintése",
"placeholder_desc": "Itt jelennek meg a rendszer által auditált események, naplóbejegyzések és rendszerszintű műveletek. A modul még fejlesztés alatt áll."
},
"config": {
"title": "Rendszer Konfig",
"subtitle": "Gamification master konfiguráció szerkesztése",
@@ -24,7 +29,18 @@
"updated": "Konfiguráció frissítve!",
"save_error": "Hiba történt a mentés során.",
"json_applied": "JSON alkalmazva a form mezőkre!",
"invalid_json": "Érvénytelen JSON: "
"invalid_json": "Érvénytelen JSON: ",
"inactivity_monitor": "Inaktivitás Figyelő",
"inactivity_desc": "Az inaktivitási küszöbérték konfigurálása automatikus fiók felfüggesztéshez. Az ennyi napja inaktív felhasználók jelölésre kerülnek az MLM Jutalék Motor által.",
"inactivity_threshold": "Inaktivitási Küszöb (nap)",
"inactivity_range": "Tartomány: 30730 nap (alapértelmezett: 180)",
"inactivity_updated": "Inaktivitási küszöb elmentve!",
"inactivity_save_error": "Nem sikerült elmenteni az inaktivitási küszöböt.",
"effective_threshold": "Hatályos Küszöb",
"last_changed": "Utolsó módosítás",
"never": "soha",
"save_inactivity": "Mentés",
"has_changes": "Változások észlelve"
},
"params": {
"title": "Rendszerparaméterek",

View File

@@ -352,6 +352,11 @@ const menuGroups: MenuGroup[] = [
label: 'menu.commission_rules',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
},
{
path: '/finance/ledger',
label: 'menu.financial_ledger',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" /></svg>',
},
],
},
],
@@ -448,15 +453,14 @@ const menuGroups: MenuGroup[] = [
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>',
},
{
path: '/system/settings',
label: 'menu.system_config',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>',
},
{
path: '/logs',
label: 'menu.system_logs',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>',
children: [
{
path: '/', /* P0 HOTFIX: placeholder until /logs page is built */
label: 'menu.system_logs',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>',
},
],
},
],
},
@@ -471,7 +475,9 @@ const pageTitle = computed(() => {
if (route.path.startsWith('/users')) return 'menu.users'
if (route.path.startsWith('/logs')) return 'menu.system_logs'
if (route.path.startsWith('/packages')) return 'menu.packages'
if (route.path.startsWith('/finance/ledger')) return 'financial_ledger.title'
if (route.path.startsWith('/finance/commission-rules')) return 'commission_rules.title'
if (route.path.startsWith('/system/settings')) return 'system.config.title'
if (route.path.startsWith('/providers')) {
if (route.path === '/providers/pending') return 'providers.pending_title'
if (route.path.startsWith('/providers/')) return 'providers.provider_detail'

View File

@@ -23,7 +23,7 @@ export default defineNuxtConfig({
files: [
'hu/common.json', 'hu/menu.json', 'hu/dashboard.json', 'hu/providers.json',
'hu/garages.json', 'hu/users.json', 'hu/gamification.json', 'hu/system.json',
'hu/commission.json',
'hu/commission.json', 'hu/finance.json',
],
},
{
@@ -31,7 +31,7 @@ export default defineNuxtConfig({
files: [
'en/common.json', 'en/menu.json', 'en/dashboard.json', 'en/providers.json',
'en/garages.json', 'en/users.json', 'en/gamification.json', 'en/system.json',
'en/commission.json',
'en/commission.json', 'en/finance.json',
],
},
{ code: 'de', iso: 'de-DE', file: 'de.json', name: 'Deutsch' },

View File

@@ -0,0 +1,387 @@
<template>
<div>
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-2xl font-bold text-white">{{ t('financial_ledger.title') }}</h1>
<p class="text-slate-400 mt-1">{{ t('financial_ledger.subtitle') }}</p>
</div>
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center py-20">
<div class="text-slate-400 flex items-center gap-3">
<svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>{{ t('financial_ledger.loading') }}</span>
</div>
</div>
<!-- Error State -->
<div v-else-if="error" class="bg-rose-500/10 border border-rose-500/30 rounded-xl p-6 mb-8">
<p class="text-rose-400">{{ t('financial_ledger.error') }}</p>
<button @click="fetchLedger" class="mt-3 text-sm text-rose-300 hover:text-rose-200 underline">
{{ t('financial_ledger.retry') }}
</button>
</div>
<template v-else>
<!-- Filters -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 mb-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
<!-- User ID Filter -->
<div>
<label class="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">
{{ t('financial_ledger.user_id') }}
</label>
<input
v-model.number="filters.user_id"
type="number"
min="1"
placeholder="User ID"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 text-sm"
@input="debouncedSearch"
/>
</div>
<!-- Transaction Type -->
<div>
<label class="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">
{{ t('financial_ledger.transaction_type') }}
</label>
<input
v-model="filters.transaction_type"
type="text"
placeholder="pl. ACCOUNT_SUSPENDED..."
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 text-sm"
@input="debouncedSearch"
/>
</div>
<!-- Entry Type -->
<div>
<label class="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">
{{ t('financial_ledger.entry_type') }}
</label>
<select
v-model="filters.entry_type"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 text-sm"
@change="fetchLedger"
>
<option value="">{{ t('financial_ledger.all_types') }}</option>
<option value="DEBIT">{{ tEnum('entry_type', 'DEBIT') }}</option>
<option value="CREDIT">{{ tEnum('entry_type', 'CREDIT') }}</option>
</select>
</div>
<!-- Wallet Type -->
<div>
<label class="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">
{{ t('financial_ledger.wallet_type') }}
</label>
<select
v-model="filters.wallet_type"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 text-sm"
@change="fetchLedger"
>
<option value="">{{ t('financial_ledger.all_wallets') }}</option>
<option value="EARNED">{{ tEnum('wallet_type', 'EARNED') }}</option>
<option value="PURCHASED">{{ tEnum('wallet_type', 'PURCHASED') }}</option>
<option value="SERVICE_COINS">{{ tEnum('wallet_type', 'SERVICE_COINS') }}</option>
<option value="VOUCHER">{{ tEnum('wallet_type', 'VOUCHER') }}</option>
</select>
</div>
<!-- Date From -->
<div>
<label class="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">
{{ t('financial_ledger.date_from') }}
</label>
<input
v-model="filters.date_from"
type="date"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 text-sm"
@change="fetchLedger"
/>
</div>
</div>
<!-- Second Row: Date To + Actions -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 mt-4">
<div>
<label class="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">
{{ t('financial_ledger.date_to') }}
</label>
<input
v-model="filters.date_to"
type="date"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 text-sm"
@change="fetchLedger"
/>
</div>
<!-- Actions -->
<div class="flex items-end gap-2">
<button
@click="clearFilters"
class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-slate-300 rounded-lg text-sm font-medium transition flex items-center gap-2"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
{{ t('financial_ledger.clear') }}
</button>
</div>
</div>
</div>
<!-- Table -->
<div class="bg-slate-800 rounded-xl border border-slate-700 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="bg-slate-700/50">
<th class="px-4 py-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider">ID</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider">{{ t('financial_ledger.user') }}</th>
<th class="px-4 py-3 text-right text-xs font-semibold text-slate-400 uppercase tracking-wider">{{ t('financial_ledger.amount') }}</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider">{{ t('financial_ledger.transaction_type') }}</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider">{{ t('financial_ledger.entry_type') }}</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider">{{ t('financial_ledger.wallet_type') }}</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider">{{ t('financial_ledger.status') }}</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider">{{ t('financial_ledger.created_at') }}</th>
</tr>
</thead>
<tbody>
<tr v-if="items.length === 0">
<td colspan="8" class="px-4 py-12 text-center text-slate-500">
<p>{{ t('financial_ledger.no_results') }}</p>
<p class="text-sm mt-1">{{ t('financial_ledger.no_results_hint') }}</p>
</td>
</tr>
<tr
v-for="entry in items"
:key="entry.id"
@click="selectedEntry = selectedEntry?.id === entry.id ? null : entry"
class="border-t border-slate-700 hover:bg-slate-700/30 transition cursor-pointer"
>
<td class="px-4 py-3 text-sm text-slate-300 font-mono">{{ entry.id }}</td>
<td class="px-4 py-3 text-sm">
<div class="text-white font-medium">{{ entry.user_name || '-' }}</div>
<div class="text-xs text-slate-500">{{ entry.user_email || '-' }}</div>
</td>
<td class="px-4 py-3 text-sm text-right">
<span
class="font-mono font-medium"
:class="entry.entry_type === 'CREDIT' ? 'text-emerald-400' : 'text-rose-400'"
>
{{ entry.entry_type === 'CREDIT' ? '+' : '-' }}{{ formatAmount(entry.amount) }}
</span>
</td>
<td class="px-4 py-3 text-sm text-slate-300">{{ tEnum('transaction_type', entry.transaction_type) }}</td>
<td class="px-4 py-3">
<span
class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full"
:class="entry.entry_type === 'CREDIT' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-rose-500/10 text-rose-400'"
>
{{ tEnum('entry_type', entry.entry_type) }}
</span>
</td>
<td class="px-4 py-3">
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-indigo-500/10 text-indigo-400">
{{ tEnum('wallet_type', entry.wallet_type) }}
</span>
</td>
<td class="px-4 py-3">
<span
class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full"
:class="statusClass(entry.status)"
>
{{ tEnum('status', entry.status) }}
</span>
</td>
<td class="px-4 py-3 text-sm text-slate-400">{{ formatDate(entry.created_at) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Expanded Details -->
<div v-if="selectedEntry" class="border-t border-slate-700 bg-slate-750 p-4">
<h3 class="text-sm font-semibold text-slate-300 mb-2">{{ t('financial_ledger.details') }}</h3>
<pre class="text-xs text-slate-400 font-mono bg-slate-900/50 rounded-lg p-3 overflow-x-auto max-h-48 overflow-y-auto">{{ JSON.stringify(selectedEntry.details, null, 2) }}</pre>
</div>
<!-- Pagination -->
<div v-if="totalPages > 1" class="flex items-center justify-between px-4 py-3 border-t border-slate-700 bg-slate-800/50">
<div class="text-sm text-slate-400">
{{ t('financial_ledger.page_info', { page: currentPage, total: totalPages, count: total }) }}
</div>
<div class="flex items-center gap-2">
<button
:disabled="currentPage <= 1"
:class="['px-3 py-1.5 text-sm rounded-lg transition', currentPage <= 1 ? 'bg-slate-700 text-slate-500 cursor-not-allowed' : 'bg-slate-700 hover:bg-slate-600 text-slate-300']"
@click="goToPage(currentPage - 1)"
>
{{ t('financial_ledger.prev') }}
</button>
<span class="text-sm text-slate-400 px-2">{{ currentPage }} / {{ totalPages }}</span>
<button
:disabled="currentPage >= totalPages"
:class="['px-3 py-1.5 text-sm rounded-lg transition', currentPage >= totalPages ? 'bg-slate-700 text-slate-500 cursor-not-allowed' : 'bg-slate-700 hover:bg-slate-600 text-slate-300']"
@click="goToPage(currentPage + 1)"
>
{{ t('financial_ledger.next') }}
</button>
</div>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCookie } from '#app'
const { t, te } = useI18n()
/**
* Translate enum values from the backend (DB enums) to the current locale.
* Falls back to the raw value if no translation key exists.
*/
function tEnum(category: string, value: string | null | undefined): string {
if (!value) return '-'
const key = `enums.${category}.${value}`
if (te(key)) return t(key)
// Try lowercase fallback for uppercase values
const lowerKey = `enums.${category}.${value.toLowerCase()}`
if (te(lowerKey)) return t(lowerKey)
return value
}
interface LedgerEntry {
id: number
user_id: number | null
person_id: number | null
amount: number
currency: string | null
transaction_type: string | null
entry_type: string | null
wallet_type: string | null
status: string | null
details: Record<string, unknown> | null
created_at: string | null
user_email: string | null
user_name: string | null
}
interface LedgerResponse {
total: number
page: number
page_size: number
items: LedgerEntry[]
}
const loading = ref(true)
const error = ref(false)
const items = ref<LedgerEntry[]>([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(50)
const selectedEntry = ref<LedgerEntry | null>(null)
const filters = ref({
user_id: null as number | null,
transaction_type: '',
entry_type: '',
wallet_type: '',
date_from: '',
date_to: '',
})
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
function getHeaders(): Record<string, string> {
const tokenCookie = useCookie('access_token')
return tokenCookie.value
? { Authorization: `Bearer ${tokenCookie.value}` }
: {}
}
function formatAmount(val: number): string {
return val.toLocaleString('hu-HU', { minimumFractionDigits: 2, maximumFractionDigits: 4 })
}
function formatDate(dateStr: string | null): string {
if (!dateStr) return '-'
const d = new Date(dateStr)
return d.toLocaleDateString('hu-HU', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
}
function statusClass(status: string | null): string {
const s = (status || '').toLowerCase()
if (s === 'completed' || s === 'success') return 'bg-emerald-500/10 text-emerald-400'
if (s === 'pending') return 'bg-amber-500/10 text-amber-400'
if (s === 'failed') return 'bg-rose-500/10 text-rose-400'
if (s === 'refunded' || s === 'refund') return 'bg-purple-500/10 text-purple-400'
if (s === 'cancelled') return 'bg-slate-500/10 text-slate-400'
return 'bg-slate-500/10 text-slate-400'
}
let debounceTimer: ReturnType<typeof setTimeout> | null = null
function debouncedSearch() {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
currentPage.value = 1
fetchLedger()
}, 400)
}
function clearFilters() {
filters.value = { user_id: null, transaction_type: '', entry_type: '', wallet_type: '', date_from: '', date_to: '' }
currentPage.value = 1
fetchLedger()
}
function goToPage(page: number) {
if (page < 1 || page > totalPages.value) return
currentPage.value = page
fetchLedger()
}
async function fetchLedger() {
loading.value = true
error.value = false
selectedEntry.value = null
try {
const params: Record<string, string | number> = {
page: currentPage.value,
page_size: pageSize.value,
}
if (filters.value.user_id) params.user_id = String(filters.value.user_id)
if (filters.value.transaction_type) params.transaction_type = filters.value.transaction_type
if (filters.value.entry_type) params.entry_type = filters.value.entry_type
if (filters.value.wallet_type) params.wallet_type = filters.value.wallet_type
if (filters.value.date_from) params.date_from = filters.value.date_from
if (filters.value.date_to) params.date_to = filters.value.date_to
const data: LedgerResponse = await $fetch('/api/v1/admin/finance/ledger', {
headers: getHeaders(),
params,
})
items.value = data.items
total.value = data.total
} catch (err) {
console.error('Failed to fetch financial ledger:', err)
error.value = true
} finally {
loading.value = false
}
}
onMounted(() => {
fetchLedger()
})
</script>

View File

@@ -215,6 +215,65 @@
</div>
</div>
<!-- Inactivity Monitor -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
{{ t('gamification.config.inactivity_monitor') }}
</h2>
<p class="text-sm text-slate-400 mb-6">
{{ t('gamification.config.inactivity_desc') }}
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Threshold Days -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-2">
{{ t('gamification.config.inactivity_threshold') }}
</label>
<div class="flex items-center gap-3">
<input
v-model.number="inactivityDays"
type="number"
min="30"
max="730"
class="w-32 px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-center focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 text-sm"
/>
<span class="text-sm text-slate-400">{{ t('gamification.config.days') }}</span>
</div>
<p class="text-xs text-slate-500 mt-1.5">
{{ t('gamification.config.inactivity_range') }}
</p>
</div>
<!-- Current effective value -->
<div class="flex items-end">
<div class="bg-slate-700/50 rounded-lg px-4 py-3 w-full">
<div class="text-xs text-slate-500 uppercase tracking-wider mb-1">{{ t('gamification.config.effective_threshold') }}</div>
<div class="text-lg font-semibold text-white">{{ inactivityDays || 180 }} {{ t('gamification.config.days') }}</div>
<div class="text-xs text-slate-500 mt-0.5">
{{ t('gamification.config.last_changed') }}: {{ lastUpdated || t('gamification.config.never') }}
</div>
</div>
</div>
</div>
<!-- Save Button -->
<div class="mt-6 flex items-center gap-4">
<button
@click="saveInactivityThreshold"
class="px-4 py-2 bg-amber-600 hover:bg-amber-500 text-white rounded-lg text-sm font-medium transition"
>
{{ t('gamification.config.save_inactivity') }}
</button>
<span v-if="inactivityDays !== originalInactivityDays" class="text-xs text-amber-400">
{{ t('gamification.config.has_changes') }}
</span>
</div>
</div>
<!-- Impact Calculator -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 mb-8">
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
@@ -387,6 +446,10 @@ const penaltyLogic = reactive<PenaltyLogic>({
const conversionLogic = reactive<ConversionLogic>({ socialToCreditRate: 100 })
const levelRewards = reactive<LevelRewards>({ creditsPer10Levels: 50 })
const inactivityDays = ref(180)
const originalInactivityDays = ref(180)
const lastUpdated = ref<string | null>(null)
const calculator = reactive({ level: 5, activities: 3, socialPoints: 50 })
const calculatedDailyXp = computed(() => {
@@ -468,6 +531,45 @@ function applyConfigToForm(cfg: Record<string, any>) {
}
}
async function fetchInactivityThreshold() {
try {
const data = await $fetch<{ key: string; value: any }>('/api/v1/system/parameters/inactivity_threshold_days?scope_level=global', {
headers: getHeaders(),
})
const val = data.value
if (typeof val === 'object' && val !== null) {
inactivityDays.value = Number(val.value || val.days || 180)
} else {
inactivityDays.value = Number(val) || 180
}
originalInactivityDays.value = inactivityDays.value
lastUpdated.value = (data as any).updated_at ? new Date((data as any).updated_at).toLocaleDateString('hu-HU') : null
} catch (e) {
// Parameter not seeded yet — default stays 180
inactivityDays.value = 180
originalInactivityDays.value = 180
}
}
async function saveInactivityThreshold() {
try {
await $fetch('/api/v1/system/parameters/inactivity_threshold_days?scope_level=global', {
method: 'PUT',
headers: getHeaders(),
body: {
value: inactivityDays.value,
description: 'User inactivity threshold in days. If last activity is older, account is suspended and MLM commission bypasses them.',
},
})
originalInactivityDays.value = inactivityDays.value
showToast(t('gamification.config.inactivity_updated'))
} catch (e) {
console.error('Failed to save inactivity threshold:', e)
formError.value = t('gamification.config.inactivity_save_error')
setTimeout(() => { formError.value = '' }, 4000)
}
}
async function fetchConfig() {
loading.value = true
error.value = false
@@ -525,6 +627,7 @@ function formatNumber(n: number): string {
onMounted(() => {
fetchConfig()
fetchInactivityThreshold()
})
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div>
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-2xl font-bold text-white">{{ t('menu.system_logs') }}</h1>
<p class="text-slate-400 mt-1">{{ t('system.logs.subtitle') }}</p>
</div>
<!-- Placeholder: System audit log viewer -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-8 text-center">
<svg class="w-16 h-16 mx-auto text-slate-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 class="text-lg font-semibold text-slate-400 mb-2">{{ t('system.logs.placeholder_title') }}</h3>
<p class="text-sm text-slate-500 max-w-md mx-auto">
{{ t('system.logs.placeholder_desc') }}
</p>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
})
const { t } = useI18n()
</script>

View File

@@ -0,0 +1,81 @@
<template>
<div>
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-2xl font-bold text-white">{{ t('system.config.title') }}</h1>
<p class="text-slate-400 mt-1">{{ t('system.config.subtitle') }}</p>
</div>
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center py-20">
<div class="text-slate-400 flex items-center gap-3">
<svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>{{ t('system.config.loading') }}</span>
</div>
</div>
<!-- Error State -->
<div v-else-if="error" class="bg-rose-500/10 border border-rose-500/30 rounded-xl p-6 mb-8">
<p class="text-rose-400">{{ t('system.config.error') }}</p>
<button @click="fetchSettings" class="mt-3 text-sm text-rose-300 hover:text-rose-200 underline">
{{ t('system.params.retry') }}
</button>
</div>
<template v-else>
<div class="space-y-6">
<!-- Raw JSON Viewer -->
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
<h2 class="text-lg font-semibold text-white mb-4">{{ t('system.config.raw_json') }}</h2>
<textarea
v-model="rawJson"
rows="8"
class="w-full px-4 py-3 bg-slate-900 border border-slate-600 rounded-lg text-sm text-slate-300 font-mono focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500"
:placeholder="t('system.config.raw_json')"
></textarea>
<div class="mt-3 flex items-center gap-3">
<button
@click="applyRawJson"
class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-slate-300 rounded-lg text-sm font-medium transition"
>
{{ t('system.config.apply_json') }}
</button>
<span v-if="jsonError" class="text-sm text-rose-400">{{ t('system.config.invalid_json') }}{{ jsonError }}</span>
<span v-if="jsonApplied" class="text-sm text-emerald-400">{{ t('system.config.json_applied') }}</span>
</div>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const loading = ref(true)
const error = ref(false)
const rawJson = ref('')
const jsonError = ref<string | null>(null)
const jsonApplied = ref(false)
onMounted(() => {
loading.value = false
})
function applyRawJson() {
jsonError.value = null
jsonApplied.value = false
try {
JSON.parse(rawJson.value) // Just validate JSON
jsonApplied.value = true
setTimeout(() => { jsonApplied.value = false }, 3000)
} catch (err) {
jsonError.value = (err as Error).message
}
}
</script>