admin_szolgáltatók_

This commit is contained in:
Roo
2026-07-01 02:27:38 +00:00
parent 189cbfd7ca
commit 6e627d0ebe
491 changed files with 20965 additions and 9703 deletions

View File

@@ -18,506 +18,308 @@ Teljes körű adatbázis konzisztencia ellenőrzés és zombie API végpontok fe
GAMIFICATION szekció létrehozása a `backend/static/locales/en.json` és `hu.json` fájlokban a `docs/gamification_i18n_audit_and_fix_proposal.md` dokumentáció alapján. GAMIFICATION szekció létrehozása a `backend/static/locales/en.json` és `hu.json` fájlokban a `docs/gamification_i18n_audit_and_fix_proposal.md` dokumentáció alapján.
### 🔧 Módosított fájlok ### 🔧 Módosított fájlok
- `backend/static/locales/en.json` - GAMIFICATION szekció hozzáadva (16 kulcs: SUBMIT_SERVICE, QUIZ, BADGE, SEASON, ACHIEVEMENTS)
- `backend/static/locales/hu.json` - GAMIFICATION szekció hozzáadva (16 kulcs, magyar fordításokkal)
### ✅ Ellenőrzés ## 2026-07-01 - Opening Hours (JSONB) Admin UI támogatás
- JSON szintaxis: ✅ Érvényes mindkét fájlban
- i18n helper `t()` függvény: ✅ Minden kulcs lekérdezhető angolul és magyarul
- Interpoláció: ✅ `{error}` helyőrzők működnek
- Leaf key count: 16 = 16 (en/hu egyezés)
## 2026-06-30 - [#344] Fázis 5/7: Backend API endpointok átalakítása - hardcoded stringek eltávolítása
### 🎯 Cél ### 🎯 Cél
A `backend/app/api/v1/endpoints/gamification.py` végpontjaiban található összes hardcoded magyar és angol szöveg cseréje az i18n `t()` helper segítségével. Nyitvatartás (Opening Hours) JSONB mező adminisztrációs támogatásának megvalósítása: backend API (GET/PATCH), frontend edit UI (7 napos grid), frontend detail display.
### 🔧 Módosított fájlok ### 🔧 Módosított fájlok
- `backend/app/api/v1/endpoints/gamification.py` - 14 helyen hardcoded string → `t("GAMIFICATION.xxx")` hívás; `from app.core.i18n import t` import hozzáadva - `backend/app/api/v1/endpoints/admin_providers.py` - ProviderDetail + ProviderUpdateInput sémák `opening_hours` mezővel; get_provider_detail() és update_provider() végpontok kiegészítése a ServiceProfile JSONB oszlop olvasásával/írásával
- `backend/static/locales/hu.json` - +10 új GAMIFICATION kulcs (QUIZ.ALREADY_COMPLETED, QUIZ.COMPLETED_SUCCESS, QUIZ.QUESTION_NOT_FOUND, QUIZ.POINTS_FAILED, BADGE.AWARDED_SUCCESS, BADGE.POINTS_FAILED, SEASON.NO_ACTIVE, XP_*_DESC, QUIZ_*_DESC) - `frontend_admin/pages/providers/[id]/edit.vue` - "Nyitvatartás" tab (4. tab) 7 napos grid-del, checkbox + time input párokkal, 24/7 gyorsgombbal, hétköznap másolással, összes törléssel
- `backend/static/locales/en.json` - +10 új GAMIFICATION kulcs angol fordítással - `frontend_admin/pages/providers/[id]/index.vue` - Nyitvatartás megjelenítő tábla a részletes nézetben
### ✅ Ellenőrzés ### ✅ Verifikáció
- Python import: ✅ `from app.api.v1.endpoints.gamification import router` sikeres - Sync Engine: 1297 OK, 0 Fixed, 0 Extra - rendszer tökéletesen szinkronban
- Szintaxis: ✅ Nincs hiba - GET /admin/providers/1 → 200, `opening_hours` mező visszatér
- i18n lefedettség: ✅ Minden korábbi hardcoded string i18n kulcsra cserélve - PATCH /admin/providers/1 `opening_hours` → 200, adatok perzisztálódnak és visszaolvashatók
- Interpoláció: ✅ `{error}`, `{badge_name}` helyőrzők működnek
## 2026-06-23 - Cost Entry Wizard (CostEntryWizard.vue) - 4-lépéses Számla Űrlap ## 2026-06-30 - [#377] M4: Többnyelvű támogatás bevezetése (DE, FR, RO, CZ, SK)
### 🎯 Cél ### 🎯 Cél
4-lépéses stepper wizard építése részletes számla/invoice költségek rögzítésére a CostsActionsCard-ból indíthatóan. 5 új nyelv (Német, Francia, Román, Cseh, Szlovák) bevezetése a backend és frontend admin i18n rendszerekbe.
### 🔧 Létrehozott fájlok
- `backend/static/locales/de.json` - Backend német lokációs fájl (7 szekció)
- `backend/static/locales/fr.json` - Backend francia lokációs fájl (7 szekció)
- `backend/static/locales/ro.json` - Backend román lokációs fájl (7 szekció)
- `backend/static/locales/cz.json` - Backend cseh lokációs fájl (7 szekció)
- `backend/static/locales/sk.json` - Backend szlovák lokációs fájl (7 szekció)
- `frontend_admin/locales/de.json` - Frontend admin német fő lokációs fájl
- `frontend_admin/locales/fr.json` - Frontend admin francia fő lokációs fájl
- `frontend_admin/locales/ro.json` - Frontend admin román fő lokációs fájl
- `frontend_admin/locales/cz.json` - Frontend admin cseh fő lokációs fájl
- `frontend_admin/locales/sk.json` - Frontend admin szlovák fő lokációs fájl
- `frontend_admin/i18n/locales/de.json` - Frontend admin német közös lokációs fájl
- `frontend_admin/i18n/locales/fr.json` - Frontend admin francia közös lokációs fájl
- `frontend_admin/i18n/locales/ro.json` - Frontend admin román közös lokációs fájl
- `frontend_admin/i18n/locales/cz.json` - Frontend admin cseh közös lokációs fájl
- `frontend_admin/i18n/locales/sk.json` - Frontend admin szlovák közös lokációs fájl
### 🔧 Módosított fájlok
- `frontend_admin/nuxt.config.ts` - 5 új nyelv regisztrálása a @nuxtjs/i18n konfigurációban
### ✅ Verifikáció
- Backend i18n: Mind az 5 nyelv betöltődik (7-7 szekció), leaf key fordítések működnek
- Sync Engine: 1293 OK, 0 Fixed, 0 Extra - rendszer tökéletesen szinkronban
### 🎯 Cél
GAMIFICATION szekció létrehozása a `backend/static/locales/en.json` és `hu.json` fájlokban a `docs/gamification_i18n_audit_and_fix_proposal.md` dokumentáció alapján.
### 🔧 Módosított fájlok ### 🔧 Módosított fájlok
## 2026-06-24 - P0 Fix & Enhance: Permissions Page Data Flow, Editing & i18n ## 2026-06-30 - [#376] M3: Admin nyelvválasztó átalakítása legördülő menüvé
### 🎯 Cél ### 🎯 Cél
Permissions oldal adatáramlás javítása, szerkesztés/mentés funkció bevezetése, és @nuxtjs/i18n telepítése/konfigurálása. A jelenlegi kétállású HU/EN kapcsoló átalakítása bővíthető, legördülő nyelvválasztó dropdownná, amely a backend-ből érkező támogatott nyelveket listázza.
### 🔧 Módosított fájlok ### 🔧 Módosított fájlok
- `frontend_admin/pages/permissions/index.vue` - Teljes átírás: client-side data fetch onMounted-ben, szerkeszthető toggle-ok (modifiedPermissions Map), PATCH mentés, toast notification, $t() hívások - `frontend_admin/components/LanguageSwitcher.vue`**LÉTREHOZVA**: Új önálló LanguageSwitcher dropdown komponens zászló emoji-kkal, nyelv nevével, localStorage + backend perzisztálással
- `frontend_admin/nuxt.config.ts` - @nuxtjs/i18n modul hozzáadása, locales konfiguráció (hu/en) - `frontend_admin/layouts/default.vue`**MÓDOSÍTVA**: 157-170. sorokban a régi gomb-alapú kapcsoló kicserélve `<LanguageSwitcher />` komponensre; felesleges `useI18n` import és `switchLocale`/`getAuthHeaders` függvények eltávolítva
- `frontend_admin/i18n/locales/hu.json` - Magyar fordítási fájl (permissions oldal összes szövege)
- `frontend_admin/i18n/locales/en.json` - Angol fordítási fájl
### Eredmények ### 📊 Eredmény
- Build: ✅ Sikeres (hu-DbjfOUfH.mjs, en-DnJ23n0t.mjs locale chunk-ok) - LanguageSwitcher komponens: ✅ Létrehozva (zászló emoji, dinamikus locale lista, backend PATCH perzisztálás, localStorage)
- i18n modul: @nuxtjs/i18n v10.4.0 telepítve, no_prefix stratégiával - default.vue cleanup: ✅ 25 sor kód eltávolítva (i18n logika kiszervezve)
- Szerkesztés: modified/original Map-ek, handleToggle, savePermissions (PATCH /admin/permissions/override/{org_id}) - Nuxt production build: ✅ Sikeres
- Adatmentés: success/error toast notification auto-clear-el
## 2026-06-24 - P0 Execution: Wire Packages UI to Real Database ## 2026-06-30 - Admin Provider Root Cause Analysis (#379)
**Vizsgálat típusa:** Rendszer-Architect root cause analysis
**Gitea kártya:** #379
### Felfedezett probléma
Az admin felület (providers list & pending queue) csak 6 rekordot mutat a `marketplace.service_providers` táblából. A robotok által gyűjtött 8569 rekord a `marketplace.service_staging`-ben láthatatlan.
### Root Cause
A `backend/app/api/v1/endpoints/admin_providers.py` `list_providers()` és `get_provider_stats()` endpointjai kizárólag a `ServiceProvider` modellt kérdezik le. A `provider_service.py` `search_providers()` (public API) már helyesen használ UNION ALL-t mindhárom forrásból.
### Javítási terv
1. `ProviderListItem` response model kiegészítése `source_table` mezővel
2. `list_providers()` UNION ALL kiterjesztése ServiceStaging-re
3. `get_provider_stats()` kiterjesztése staging rekordokkal
4. Nem kell új validációs mező - a `provider_validations`, `service_profiles.trust_score` stb. már létezik
### Dokumentáció
`docs/admin_providers_root_cause_analysis.md`
## 2026-06-30 - Admin Provider Discovery Implementáció
### 🎯 Cél ### 🎯 Cél
A frontend_admin Packages oldal (packages/index.vue) mock adatok helyett valós API adatbázisból töltse a SubscriptionTier csomagokat. "Új Csomag Létrehozása" gomb és modal hozzáadása. A `list_providers()` admin végpont kiterjesztése UNION ALL lekérdezéssel, hogy a bot által gyűjtött ServiceStaging rekordok (8569 db) és a verified organization-ök is megjelenjenek az admin felületen. Új `rejected.vue` oldal az elutasított szolgáltatók visszaállításához.
### 🔧 Változtatások
1. `backend/app/api/v1/endpoints/admin_providers.py`:
- `_build_unified_providers_query()`: UNION ALL a ServiceProvider, ServiceStaging és Organizations táblákból
- `source_filter` és `source_table_filter` a UNION ALL eredményére alkalmazva (nem az egyes részekre) - kijavítva a SourceType enum ütközési hibát
- `get_provider_stats()`: staging_pending mező hozzáadása
- `restore_provider()`: POST végpont rejected->approved státuszváltáshoz
- `get_provider_history()`, `update_provider()` végpontok
2. `frontend_admin/pages/providers/rejected.vue`: új oldal az elutasított szolgáltatók listázásához és visszaállításához
3. `frontend_admin/pages/providers/index.vue`: source_table, bot/verified_org szűrés támogatás
4. `frontend_admin/pages/providers/pending.vue`: source_table mező hozzáadása
### ✅ Tesztek
- `source=bot` filter: 200 OK, 5 staging rekord visszaadva
- `source=verified_org` filter: 200 OK, 1 organization rekord
- `source_table=service_staging` filter: 200 OK, 3 staging rekord
- Stats: total=8575, staging_pending=5096
- Rejected filter: 200 OK, 2 rejected rekord
## 2026-06-30 - Provider Admin Tabbed Edit UI (Status, Validations, History)
### 🎯 Cél
A szolgáltatók admin felületének bővítése: többfülű szerkesztési nézet, státuszkezelés, gamification validációs lista és állapotváltoztatási előzmények megjelenítése.
### 🔧 Módosított fájlok ### 🔧 Módosított fájlok
- `backend/app/schemas/subscription.py` - `tier_level` és `feature_capabilities` mezők hozzáadva a SubscriptionTierResponse, SubscriptionTierCreate, SubscriptionTierUpdate modellekhez
- `backend/app/api/v1/endpoints/admin_packages.py` - create/update végpontok frissítve az új mezők kezelésére
- `frontend_admin/pages/packages/index.vue` - Teljes átírás: mock adatok eltávolítva, fetchPackages() API hívás, create/edit/delete modal-ok, loading/error/empty állapotok, i18n támogatás
- `frontend_admin/locales/hu.json` - packages i18n kulcsok (magyar)
- `frontend_admin/locales/en.json` - packages i18n kulcsok (angol)
## 2026-06-29 - #327 Gitea Kártya: Backend - Meglévő PATCH /admin/users/{id}/penalty endpoint javítása **1. `backend/app/api/v1/endpoints/admin_providers.py`**
- `ProviderUpdateInput` séma bővítése: `status` (pending/approved/rejected) és `validation_score` (0-100) mezőkkel
- `ProviderValidationEntry` response séma létrehozása (id, provider_id, voter_user_id, validation_type, weight, validation_metadata, created_at, voter_name)
- `GET /admin/providers/{id}/validations` végpont: gamification validációk lekérése voter névvel
- `PATCH /admin/providers/{id}` módosítása: státuszváltás kezelése (ModerationStatus enum), old_data audit snapshot, validation_score auto-állítás restore/reject esetén
**2. `frontend_admin/pages/providers/[id].vue`**
- Teljes átírás: egysávos layout helyett 4 fül (Adatok, Státusz, Validációk, Előzmények)
- **Adatok fül**: változatlan alapadatok, cím, elérhetőség, moderációs műveletek
- **Státusz fül**: új űrlap státusz select, validation_score range slider (0-100), indoklás textarea
- **Validációk fül**: gamification validációs lista típus-badge-ekkel, szavazó név, súly, metadata, timestamp
- **Előzmények fül**: idővonal action-badge-ekkel, státusz átmenetek (régi→új), user info, expandálható részletek
- Elutasított/Megjelölt szekció: "Szerkesztés" és "Visszaállítás" gombok minden státuszhoz
### ✅ Verifikáció
- Sync Engine: ✅ 1293 OK, 0 Fixed, 0 Extra - teljes szinkron
- Backend syntax: ✅ admin_providers.py AST parse OK
- Backend import: ✅ router modul betöltés sikeres
## 2026-06-30 - P0 Rescue: Admin Providers API Blocker Fixes
### 🎯 Cél ### 🎯 Cél
A #327-es Gitea kártya (Epic 8 Gamification 2.0, 4. implementációs lépés) feladata a meglévő `PATCH /admin/users/{user_id}/penalty` végpont hibáinak kijavítása. Az endpoint már refaktorálva volt GamificationService `process_activity()` használatára, de 3 hibát tartalmazott. 4 darab kritikus blokkoló hiba javítása az Admin Providers flow-ban.
### 🔧 Javított hibák
**1. Hiányzó `import logging` az [`admin.py`](backend/app/api/v1/endpoints/admin.py)-ban**
- `logger.error()` hívás volt a fájlban (483. sor), de `import logging` nem szerepelt
- Javítás: `import logging` és `logger = logging.getLogger("admin-endpoints")` hozzáadva
**2. Undefined variable `admin` a `approve_action` endpointban**
- 82. sor: `admin.id` helyett `current_user.id` (az `admin` változó sosem volt definiálva)
**3. Rossz [`SecurityAuditLog`](backend/app/models/system/audit.py:12) mezőnevek a penalty endpointban (464-471. sor)**
- `user_id=current_user.id``actor_id=current_user.id`
- `target_user_id=user_id``target_id=user_id`
- `details="..."``payload_after={"penalty_points_added": ..., "reason": ...}` (JSON dict, nem string)
- `ip_address="admin_api"` → eltávolítva (nem létező mező)
- `payload_before={}` hozzáadva (kötelező mező)
### 🐛 Hibakeresési tapasztalat
- A kezdeti 500-as hibaüzenet félrevezető volt: `'UserStats' object has no attribute 'level'` - valójában a `current_level` mező helyesen létezik
- A valódi hiba a `SecurityAuditLog` létrehozásánál volt, de a `raise e` minta a [`process_activity()`](backend/app/services/gamification_service.py:174)-ben elnyelte a belső traceback-et
- Stratégiai debug logging segítségével sikerült lokalizálni a valódi hibát
- `.pyc` cache törlése és konténer újraindítás szükséges volt a friss kód betöltéséhez
### ✅ Eredmények
- HTTP 200: `{"status":"success","message":"Gamification penalty applied to user 1","user_id":1,"penalty_points_added":100,"total_penalty_points":300,"restriction_level":1}`
- Adatbázis: user 1 `penalty_points=300`, `restriction_level=1`
- Audit log: `audit.security_audit_logs` bejegyzés `action="apply_gamification_penalty"`, `actor_id=2`, `target_id=1`
- Sync Engine: ✅ 1284 OK, 0 Fixed, 0 Extra
- Debug logging eltávolítva a [`gamification_service.py`](backend/app/services/gamification_service.py)-ból
- Gitea #327: ✅ Lezárva
## 2026-06-29 - Gitea #328: Gamification Admin Integration - Bugfixes & Endpoint Verification
### 🎯 Cél
Gitea #328 ("Backend: gamification.py endpointok Gami") kártya feladatainak befejezése: admin gamification végpontok hibáinak javítása és teljes körű tesztelése.
### 🔧 Javítások
**1. [`gamification.py`](backend/app/api/v1/endpoints/gamification.py) - submit-service végpont hibák:**
- Hiányzó `logger` import hozzáadva (`import logging`, `logger = logging.getLogger("gamification-endpoints")`)
- `metadata={...}``provided_fields={...}` javítás (a `UserContribution` modellben `provided_fields` a JSONB mező neve)
- Hiányzó kötelező mezők hozzáadva: `action_type=1`, `earned_xp=submission_rewards.get("xp", 100)`
**2. [`admin_gamification.py`](backend/app/api/v1/endpoints/admin_gamification.py) - master-config végpont javítás:**
- `system_service.get_setting()``system_service.get_scoped_parameter()` (a `SystemService`-ben nincs `get_setting` metódus)
- `system_service.set_setting()``system_service.set_scoped_parameter()` ugyanezen okból
**3. [`system_service.py`](backend/app/services/system_service.py) - set_scoped_parameter upsert logika javítás:**
- Az eredeti `ON CONFLICT ON CONSTRAINT uix_param_scope` nem működött `scope_id IS NULL` esetén, mert PostgreSQL-ben a UNIQUE constraint nem blokkolja a NULL értékű duplikátumokat (NULL != NULL)
- Kétlépéses megközelítésre váltva: először megkeresi a meglévő rekordot, ha van frissíti, ha nincs beszúrja
- Adatbázis: `system.system_parameters.scope_level` oszlop típusa átállítva custom PostgreSQL enum-ról `VARCHAR(50)`-re a SQLAlchemy String(50) modellel való kompatibilitás miatt
### ✅ Teszt Eredmények
- **Admin Gamification végpontok:** 18/18 ✅ (GET/POST/PUT/DELETE point-rules, level-configs, badges, seasons, competitions, user-stats, master-config, system-params, points-ledger, contributions)
- **Penalty/Reward végpontok:** ✅ POST `/admin/gamification/user-stats/{id}/penalty` és `/reward`
- **Legacy PATCH `/admin/users/{id}/penalty`:** ✅ Továbbra is működik a GamificationService-en keresztül
- **Sync Engine:** ✅ 1284 OK, 0 Fixed, 0 Extra
- **Gitea #328:** ✅ Lezárva
## 2026-06-29 - #329 Frontend: Gamification menüpont + routing
### 🎯 Cél
Gamification menüpont és routing létrehozása a Nuxt 3 admin frontendben.
### 📝 Megvalósítás
- **`frontend_admin/layouts/default.vue`:** Gamification menücsoport beszúrása a sidebar `menuGroups` tömbjébe 6 menüelemmel (Játékmenet Beállítások, Események & Szezonok, Felhasználói Adatok, Gamification HQ, Rendszer Konfig, Rendszerparaméterek)
- **`frontend_admin/pages/gamification/`:** 12 oldal létrehozása:
- `index.vue` — Gamification HQ Dashboard (stat kártyák, gyors műveletek, pontnapló, top 5 ranglista)
- `point-rules.vue`, `levels.vue`, `badges.vue` — Játékmenet beállítások
- `seasons.vue`, `competitions.vue` — Események & Szezonok
- `users/index.vue`, `users/[id].vue` — Felhasználói statisztikák
- `leaderboard.vue`, `ledger.vue` — Ranglista és Pontnapló
- `config.vue`, `parameters.vue` — Rendszer konfiguráció
- **Routing:** Nuxt 3 file-based routing automatikusan generálja a route-okat
- **Build:** Sikeres Nuxt build (Vite + Nitro), konténer újraindítva
- **Gitea #329:** ✅ Elkészült
## 2026-06-29 - Gitea #330: Gamification Dashboard (Frontend Admin)
### 🎯 Cél
Gamification HQ Dashboard oldal (`/gamification/index.vue`) elkészítése az admin felülethez, stat kártyákkal, gyors műveletekkel, pontnaplóval és top 5 ranglistával.
### 🔧 Módosított fájlok
- **`backend/app/api/v1/endpoints/admin_gamification.py`:**
- Új `GET /admin/gamification/dashboard-summary` végpont hozzáadva, ami összesíti: összes felhasználó, aktív szezon neve, függő hozzájárulások száma, mai XP
- `list_points_ledger` végpont javítva: `user_name` mező hozzáadva (User.email JOIN-nel)
- `datetime.utcnow()``datetime.now(timezone.utc)` deprecation fix
- **`frontend_admin/pages/gamification/index.vue`:**
- API hívás átirányítva a dedikált `/api/v1/admin/gamification/dashboard-summary` végpontra
- Leaderboard adatok mapping-je kiegészítve: `username`/`current_level`/`total_xp` mezők támogatása a backend válaszhoz igazodva
- **Sync Engine:** ✅ 1284 elem szinkronban, 0 hiba
- **Gitea #330:** ✅ Elkészült
## 2026-06-29 - Gitea #331: Frontend Pontszabályok + Szintek CRUD oldal (admin)
### 🎯 Cél
A Gamification admin felület Pontszabályok és Szintek CRUD oldalainak implementálása a Nuxt 3 admin frontendben.
### 🔧 Változtatások ### 🔧 Változtatások
- **`frontend_admin/pages/gamification/point-rules.vue`:** **FIX 1 - Silent Rollback (PATCH endpoint):**
- Teljes CRUD implementáció: listázás, létrehozás, szerkesztés, soft-delete (is_active=False) - [`update_provider()`](backend/app/api/v1/endpoints/admin_providers.py:1147) `db.commit()` köré `try/except IntegrityError` blokk került
- Adattábla: ID, action_key, points, description, is_active oszlopok - IntegrityError esetén `db.rollback()` + `HTTPException(400)` a csendes 200 OK helyett
- Create/Edit modal: action_key, points, description, is_active mezők validációval
- Delete confirmation modal soft-delete-hez
- 409 Conflict kezelés (duplikált action_key)
- Toast értesítések minden művelethez
- API végpont: `/api/v1/admin/gamification/point-rules`
- **`frontend_admin/pages/gamification/levels.vue`:** **FIX 2 - Unique Constraint on Restore:**
- Teljes CRUD implementáció: listázás, létrehozás, szerkesztés - [`_create_provider_validation()`](backend/app/api/v1/endpoints/admin_providers.py:198) átírva PostgreSQL `INSERT ... ON CONFLICT DO UPDATE` (UPSERT) használatára
- Adattábla: level_number, rank_name, min_points, is_penalty oszlopok - A `uq_voter_provider_validation` constraint tiszteletben tartva
- Create/Edit modal: level_number, rank_name, min_points, is_penalty mezők validációval
- Vizuális szint fa diagram progress bar-okkal
- 409 Conflict kezelés (duplikált level_number)
- Toast értesítések minden művelethez
- API végpont: `/api/v1/admin/gamification/level-configs`
- **Build:** ✅ Sikeres Nuxt build a `sf_admin_frontend` konténerben **FIX 3 - Wrong Frontend URL:**
- **Gitea #331:** ✅ Elkészült - Ellenőrizve: a frontend [`[id].vue`](frontend_admin/pages/providers/[id].vue:761) már a helyes `/api/v1/admin/providers/{id}/validations` URL-t használja
## 2026-06-29 - Gitea #332: Frontend: Kitüntetések (Badge) kezelő oldal **FIX 4 - AuditLog Attribute Error:**
- [`get_provider_history()`](backend/app/api/v1/endpoints/admin_providers.py:1029) `AuditLog.created_at``AuditLog.timestamp`
- `timestamp=log.created_at``timestamp=log.timestamp`
### ✅ Verifikáció
- Backend syntax: ✅ `py_compile` OK
- Modul import: ✅ `admin_providers` modul betöltés sikeres
- Container: ✅ `sf_api` restart sikeres, státusz Up
## 2026-06-30 - [#381] P0 Fix: Provider validations 500 error & missing edit/flag modals
### 🎯 Cél ### 🎯 Cél
A Gamification admin felület Kitüntetések (Badge) CRUD oldalának implementálása a Nuxt 3 admin frontendben. A backend API végpontok már léteztek a [`admin_gamification.py`](backend/app/api/v1/endpoints/admin_gamification.py)-ban (GET/POST/PUT/DELETE `/admin/gamification/badges`). Két P0 hibajavítás az Admin Provider Details oldalon.
### 🔧 Módosított fájlok ### 🔧 Módosított fájlok
- **`frontend_admin/pages/gamification/badges.vue`:** **Bug #1 - `get_provider_validations()` 500 error:**
- Teljes CRUD implementáció: listázás, létrehozás, szerkesztés, törlés - [`admin_providers.py`](backend/app/api/v1/endpoints/admin_providers.py:1018) — `v.voter.display_name``v.voter.email`
- Adattábla: ID, Név, Leírás, Ikon (kép előnézettel), Műveletek oszlopok - Root cause: `User` modellben nincs `display_name` mező
- Create/Edit modal: name (kötelező, max 100), description (kötelező, max 500), icon_url (opcionális URL élő előnézettel)
- Delete confirmation modal: badge név és leírás megjelenítése
- 409 Conflict kezelés (duplikált név)
- Toast értesítések minden művelethez (siker/hiba)
- Loading, error és empty állapotok kezelése
- Auth middleware (`useAuth()`)
- API végpont: `/api/v1/admin/gamification/badges`
### ✅ Eredmények **Bug #2 - Dead "Szerkesztés" gomb:**
- **GET /admin/gamification/badges:** ✅ 200 OK (2 meglévő badge listázva) - [`[id].vue`](frontend_admin/pages/providers/[id].vue:540) — Edit modal template hozzáadva (name, city, address, contact, category mezőkkel)
- **POST /admin/gamification/badges:** ✅ 201 Created (új badge létrehozva egyedi névvel) - [`[id].vue`](frontend_admin/pages/providers/[id].vue:603) — Flag modal template hozzáadva (indoklás textarea-val)
- **PUT /admin/gamification/badges/{id}:** ✅ 200 Updated (badge adatok módosítva) - Root cause: A `<template>` szekcióból hiányzott mindkét modal, bár a JavaScript logika (`showEditModal`, `openEditModal`, `saveEdit`, `showFlagModal`, `flagProvider`) definiálva volt
- **DELETE /admin/gamification/badges/{id}:** ✅ 204 No Content (badge törölve, GET visszaadja a maradékot)
- **Sync Engine:** ✅ Ellenőrizve
- **Gitea #332:** ✅ Elkészült
## 2026-06-29 - Gitea #333: Frontend Szezonok + Versenyek kezelő oldal ### ✅ Verifikáció
- Backend syntax: ✅ `ast.parse` OK
- Frontend template: ✅ Mind a 3 modal (Reject, Edit, Flag) jelen van a `<template>`-ben
## 2026-07-01 - [#382] P0: Backend Database Models - 3D Filtering Matrix
### 🎯 Cél ### 🎯 Cél
A gamification admin felület Szezonok (seasons) és Versenyek (competitions) kezelő oldalainak implementálása a meglévő backend API végpontokra építve. A ServiceProvider és ServiceProfile modellek bővítése `supported_vehicle_classes` (ARRAY) és `specializations` (JSONB) oszlopokkal a 3D szűrési mátrix támogatásához. Pydantic schemák frissítése az új mezőkkel.
### 🔧 Módosított fájlok ### 🔧 Módosított fájlok
- **`frontend_admin/pages/gamification/seasons.vue`** - Teljes CRUD implementáció: kártya nézet, create/edit modal, aktiválás megerősítéssel, státusz badge-ek (active/inactive), dátumtartomány megjelenítés
- **`frontend_admin/pages/gamification/competitions.vue`** - Teljes CRUD implementáció: grid kártya nézet, szezon szűrő dropdown, create/edit modal JSON rules szerkesztővel, státusz badge-ek (draft/active/completed/cancelled)
### ✅ Eredmény **1. [`backend/app/models/identity/social.py`](backend/app/models/identity/social.py:55)**
- Build: ✅ Sikeres (Nuxt build hiba nélkül) - `ARRAY` import hozzáadva `sqlalchemy.dialects.postgresql`-ből
- Backend API: már implementálva (`admin_gamification.py`) - `supported_vehicle_classes: Mapped[Optional[list]] = mapped_column(ARRAY(String), server_default=text("'{}'"), nullable=True)`
- Menüpontok: már konfigurálva (`default.vue`) - `specializations: Mapped[Optional[dict]] = mapped_column(JSONB, server_default=text("'{}'::jsonb"), nullable=True)`
- **Gitea #333:** ✅ Elkészült
## 2026-06-29 - Gitea #334: Frontend: Felhasználói statisztikák + büntetés admin nézet (penalty) **2. [`backend/app/models/marketplace/service.py`](backend/app/models/marketplace/service.py:40)**
- `ARRAY` import hozzáadva `sqlalchemy.dialects.postgresql`-ből
- Azonos 2 oszlop hozzáadva a ServiceProfile modellhez
**3. [`backend/app/api/v1/endpoints/admin_providers.py`](backend/app/api/v1/endpoints/admin_providers.py:126)**
- `ProviderUpdateInput`: `category_ids`, `supported_vehicle_classes`, `specializations` mezők
- `ProviderDetail`: `category_ids`, `supported_vehicle_classes`, `specializations` mezők
- `get_provider_detail()` és `update_provider()` response-ok frissítve
**4. [`backend/app/schemas/provider.py`](backend/app/schemas/provider.py:137)**
- `ProviderQuickAddIn`: `supported_vehicle_classes`, `specializations` mezők
- `ProviderUpdateIn`: `supported_vehicle_classes`, `specializations` mezők
- `ProviderSearchResult`: `supported_vehicle_classes`, `specializations` mezők
### ✅ Verifikáció
- Sync Engine: ✅ 1293 OK, 4 Fixed (4 új oszlop), 0 Extra
- Adatbázis oszlopok ellenőrizve: `marketplace.service_providers.supported_vehicle_classes` (ARRAY), `marketplace.service_providers.specializations` (JSONB), `marketplace.service_profiles.supported_vehicle_classes` (ARRAY), `marketplace.service_profiles.specializations` (JSONB) — mind jelen van megfelelő típussal és default értékkel
## 2026-07-01 - [#383] P0: Admin PATCH endpoint - ServiceExpertise sync & Array persistence
### 🎯 Cél ### 🎯 Cél
A Gamification admin felület felhasználói statisztikák oldalának (`users/index.vue`) és felhasználó részletes nézetének (`users/[id].vue`) implementálása, büntetés/jutalom funkciókkal. A `PATCH /admin/providers/{id}` végpont kiegészítése: `category_ids`, `supported_vehicle_classes`, `specializations` mezők mentése mind a ServiceProvider, mind a ServiceProfile modellekbe, valamint `_sync_service_expertises()` meghívása a ServiceProfile ID-val a taxonomy fa szinkronizálásához.
### 🔧 Eredmények
**1. BACKEND ELLENŐRZÉS:** ✅ PASS
- `admin_gamification.py` router már teljesen implementálva volt (1098 sor)
- Minden szükséges végpont létezik: user-stats list/get/update, penalty POST, reward POST, points-ledger, contributions
- `GamificationService.process_activity()` támogatja a penalty/reward admin műveleteket
**2. FRONTEND `users/index.vue`:** ✅ Implementálva
- Felhasználói statisztikák adattábla (XP, szint, pontok, büntetés, restrikció, aktivitás)
- Szűrők: szint, minimum/maximum XP, büntetés státusz
- Lapozás (offset/limit alapú)
- Linkek a részletes nézetre
**3. FRONTEND `users/[id].vue`:** ✅ Implementálva
- Statisztika kártyák: XP, szint, pontok, social pont, büntetés, restrikció, kvóta, kitiltás
- Aktivitás mutatók: szolgáltatások, felfedezések, validációk, szolgáltatók
- **Büntetés űrlap:** POST `/admin/gamification/user-stats/{id}/penalty` (amount + reason)
- **Jutalom űrlap:** POST `/admin/gamification/user-stats/{id}/reward` (xp_amount + social_amount + reason)
- Pontnapló tábla a felhasználóhoz tartozó ledger bejegyzésekkel
**4. ADATBÁZIS SZINKRONIZÁCIÓ:** ✅ PASS
- sync_engine: 1284 elem OK, 0 hiba, 0 extra elem
- A rendszer tökéletesen szinkronban van
### ✅ Eredmény
- Backend: ✅ Már implementálva (admin_gamification.py)
- Frontend users/index.vue: ✅ Teljes implementáció (szűrők, tábla, lapozás)
- Frontend users/[id].vue: ✅ Teljes implementáció (statok, büntetés, jutalom, ledger)
- Adatbázis: ✅ Szinkronban
- **Gitea #334:** ✅ Elkészült
## 2026-06-29 - Gitea #335: Frontend Ranglista Admin Nézet (Leaderboard)
### 🎯 Cél
Admin ranglista nézet megvalósítása a gamification rendszerhez: backend API végpont + frontend admin oldal.
### 🔧 Eredmények
**1. BACKEND - Admin Leaderboard API végpont:**
- `GET /admin/gamification/leaderboard` végpont hozzáadva az [`admin_gamification.py`](backend/app/api/v1/endpoints/admin_gamification.py:1193) fájlhoz
- Támogatja: pagination (limit/offset), filtering (level, min_xp, search), sorting (sort_by/sort_order)
- Kétlépcsős eager loading: `joinedload(UserStats.user).joinedload(User.person)` a Person nevek eléréséhez
- Explicit `onclause` a Person join-nál az `AmbiguousForeignKeysError` elkerülésére (User↔Person bidirekcionális FK)
- `or_` import hozzáadva a SQLAlchemy import sorhoz
**2. FRONTEND - Leaderboard oldal:**
- [`leaderboard.vue`](frontend_admin/pages/gamification/leaderboard.vue) teljes implementáció:
- Keresés név/email alapján
- Szint filter (1-20), minimum XP filter
- Rendezés (XP, Pontok, Social Points, Szint) + irány toggle
- Adattábla 13 oszloppal (Rank, Név, Email, XP, Szint, Pontok, Social, Penalty, Restriction, Felfedezések, Szolgáltatások, Frissítve, Műveletek)
- Rank badge-ek (arany/ezüst/bronz) top 3 helyezéshez
- Lapozás first/prev/next/last + oldalméret választó (25/50/100/250)
- Loading, error, empty állapotok
- Link a felhasználói részletek oldalra
**3. TESZTELÉS:**
- Alap leaderboard lekérdezés: 12 user, XP szerint csökkenő
- Szint filter (level=2): 7 találat
- Min XP filter (min_xp=4000): 2 találat
- Rendezés ascending (current_level): helyes sorrend
- Keresés névre ("accip"): 1 találat (Tímea Accipe)
- Keresés emailre ("profibot"): 6 találat
- Keresés vezetéknévre ("Gyöngyössy"): 2 találat
- Kombinált filter (level=2 & search=accip): 1 találat
- Lapozás (offset=5, limit=3): helyes rank számozás
- **Gitea #335:** ✅ Elkészült
## 2026-06-29 - Gitea #336: Frontend Master Config & System Parameters Pages
### 🎯 Cél
Két admin frontend oldal implementálása a Gamification 2.0 modulhoz:
1. `config.vue` — GAMIFICATION_MASTER_CONFIG JSON szerkesztő hatás kalkulátorral
2. `parameters.vue` — Gamification kategóriájú SystemParameter-ek DataTable inline szerkesztéssel
### 🔧 Módosított fájlok ### 🔧 Módosított fájlok
- `frontend_admin/pages/gamification/config.vue` — Teljes implementáció (~540 sor): 4 konfigurációs szekció (XP Logic, Penalty Logic, Conversion Logic, Level Rewards) csúszkás/inputos szerkesztéssel, Impact Calculator szint/aktivitás/szociális pont csúszkákkal, valós idejű számított értékekkel (napi XP, szintlépés XP, szociális kredit, napok a szintlépésig), Raw JSON szerkesztő validációval, mentés PUT /admin/gamification/master-config végpontra
- `frontend_admin/pages/gamification/parameters.vue` — Teljes implementáció (~280 sor): DataTable key/value/category/scope_level/scope_id oszlopokkal, inline szerkesztő modal textarea-val, smart value parsing (JSON/Number/String auto-detection), mentés PUT /admin/gamification/system-params/{key} végpontra
### ✅ Eredmények **1. [`backend/app/api/v1/endpoints/admin_providers.py`](backend/app/api/v1/endpoints/admin_providers.py:1098)**
- Backend API-k auditálva: GET/PUT /admin/gamification/master-config és GET/PUT /admin/gamification/system-params végpontok teljesen implementálva - **Importok bővítése**: `ServiceProfile` import `app.models.marketplace.service`-ből, `_sync_service_expertises` import `app.services.provider_service`-ből
- Mindkét oldal Nuxt HMR-rel betöltődött és fordítási hiba nélkül fut a sf_admin_frontend konténerben (port 8502) - **`update_provider()`**: `category_ids`, `supported_vehicle_classes`, `specializations` kiemelése a payload-ból `update_fields.pop()` segítségével; tömbök mentése ServiceProvider-re; ServiceProfile lekérése `service_provider_id` alapján; tömbök mentése ServiceProfile-ra; `_sync_service_expertises(db, profile.id, category_ids)` hívása
- Auth pattern: useCookie('access_token') + Authorization: Bearer header - **`get_provider_detail()`**: `category_ids` feloldása a ServiceExpertise join táblából ServiceProfile-on keresztül (korábban hardcoded `None`)
- Dark theme: slate-800/emerald/amber/rose/violet/cyan színséma
- **Gitea #336:** ✅ Elkészült
## 2026-06-29 - Gitea #337: Frontend Pontnapló böngésző oldal (ledger.vue) ### ✅ Verifikáció
- Backend syntax: ✅ `py_compile` OK (exit code 0)
- Container restart: ✅ `sf_api` sikeresen újraindítva
- Import verifikáció: ✅ `ProviderUpdateInput` séma tartalmazza a `category_ids`, `supported_vehicle_classes`, `specializations` mezőket
## 2026-07-01 - [#384] P0: Service Provider Dedicated Full-Page Editor
### 🎯 Cél ### 🎯 Cél
PointsLedger admin böngésző oldal elkészítése DataTable-lel, szűrőkkel, lapozással és CSV exporttal. Dedikált teljes oldalas szerkesztő létrehozása a szolgáltatók adminisztrációjához, amely felváltja a hiányzó modális szerkezetet. A szerkesztő 3 tabos struktúrával rendelkezik: Alapadatok & Kapcsolat, Szolgáltatások & Járművek (3D Mátrix), Helyszín & Lokáció.
### 🔧 Létrehozott fájlok
- `frontend_admin/pages/providers/[id]/edit.vue` - Teljes oldalas 3-tabos szerkesztő (Alapadatok, 3D Mátrix, Helyszín)
- `frontend_admin/components/TreeNode.vue` - Rekurzív fa komponens a kategória hierarchia megjelenítéséhez
### 🔧 Módosított fájlok ### 🔧 Módosított fájlok
- `backend/app/api/v1/endpoints/admin_gamification.py` - `reason_search` paraméter hozzáadása a `GET /admin/gamification/points-ledger` végponthoz (ILIKE keresés a reason mezőben) - `frontend_admin/pages/providers/[id].vue` - "Szerkesztés" gomb átirányítása a modális helyett az új `/providers/{id}/edit` útvonalra
- `frontend_admin/pages/gamification/ledger.vue` - Teljes implementáció: loading/error/empty állapotok, szűrősáv (user_id, date_from, date_to, reason_search), adattábla (ID, User, Pont, Büntetés, XP, Ok, Forrás, Dátum), lapozás (Előző/Következő), CSV export, toast értesítések
### ✅ API Teszt Eredmények ### 📝 Technikai részletek
- `GET /api/v1/admin/gamification/points-ledger?limit=5` → 200 OK (5 entry) - **Tab 1 (Alapadatok & Kapcsolat):** Név, Kategória, Forrás (read-only), Telefon, Email, Weboldal
- `GET /api/v1/admin/gamification/points-ledger?reason_search=test&limit=5` → 200 OK (5 entry, ILIKE filter működik) - **Tab 2 (Szolgáltatások & Járművek):** 3D Képesség Mátrix - A: Járműosztályok (8 checkbox), B: Szolgáltatási Kategóriák (rekurzív fa), C: Specializációk (márka + hajtáslánc tag inputok)
- `GET /api/v1/admin/gamification/points-ledger?user_id=1&limit=5` → 200 OK (4 entry) - **Tab 3 (Helyszín & Lokáció):** Teljes cím, Város, Irányítószám, Utca, Házszám, Plus Code
- **API integráció:** PATCH `/api/v1/admin/providers/{id}` végpont használata a `ProviderUpdateInput` séma szerint
- **Adatfolyam:** GET provider detail → form mapping → user edit → PATCH save → visszanavigálás a detail oldalra
- **Build:** Sikeres Nuxt build hibák nélkül
- **Gitea #337:** ✅ Elkészült ## 2026-07-01 - P0 HOTFIX: Edit page data modification fix (3 bugs)
## 2026-06-29 - Gamification Admin E2E Teszt Suite (#338)
### 🎯 Cél ### 🎯 Cél
Teljes körű E2E teszt suite létrehozása a Gamification admin felülethez, 10 tesztforgatókönyvvel. A `/providers/[id]/edit` oldalon a felhasználó nem tudott adatokat módosítani és menteni. 3 összefüggő hibát azonosítottam és javítottam.
### 🔧 Javítások
**1. Backend - Hiányzó `address` mező a `ProviderUpdateInput` sémában**
- Hely: `backend/app/api/v1/endpoints/admin_providers.py:127`
- Probléma: A PATCH végpont Pydantic sémája nem tartalmazta az `address` mezőt, így a "Teljes cím" módosítása sosem került mentésre.
- Javítás: `address: Optional[str] = Field(None, max_length=500)` hozzáadva a sémához.
**2. Frontend - Hiányzó `address` és `address_street_type` a `hasChanges` computed-ból**
- Hely: `frontend_admin/pages/providers/[id]/edit.vue:579`
- Probléma: A Mentés gomb disabled maradt, ha csak a "Teljes cím" vagy "Utca típusa" mezőt módosították.
- Javítás: `form.address !== (p.address || '')` és `form.address_street_type !== (p.address_street_type || '')` ellenőrzések hozzáadva.
**3. Frontend - Hiányzó `address` a `saveForm` payload-ból**
- Hely: `frontend_admin/pages/providers/[id]/edit.vue:803`
- Probléma: A PATCH kérés törzséből hiányzott az `address` mező.
- Javítás: `address: form.address || null` hozzáadva a payload objektumhoz.
### ✅ Verifikáció
- Backend Python szintaxis: OK
- Sync Engine: 1297 elem OK, 0 hiba, teljes szinkronban
---
## 2026-07-01 - P0 Feature: "Always Open" (0-24) nyitvatartás a provider admin felületen
### 🎯 Cél
A provider admin szerkesztő felületen egy "Folyamatos (0-24) nyitvatartás" checkbox bevezetése, amely:
- Backend: `is_always_open: Optional[bool]` mező a `ProviderUpdateInput` sémában
- Backend: Ha `is_always_open=True`, a `update_provider()` PATCH endpoint automatikusan 00:00-23:59-re állítja mind a 7 napot, és `always_open: true` metadatát tárol a JSONB `opening_hours` mezőben
- Backend: Ha `is_always_open=False`, a meglévő `opening_hours` mentése változatlan, az `always_open` kulcs törlődik
- Frontend: Checkbox a Nyitvatartás tabon, amely bekapcsoláskor letiltja a naponkénti bevitelt és 00:00-23:59-re állít mindent
- Adatbázis: Nincs új migráció - a `always_open` metadata a meglévő JSONB `opening_hours` oszlopban tárolódik
### 🔧 Módosított fájlok ### 🔧 Módosított fájlok
- `backend/tests/e2e/test_gamification_admin_e2e.py` - LÉTREHOZVA: 995 soros E2E teszt suite
### 📋 Teszt forgatókönyvek **1. Backend - `backend/app/api/v1/endpoints/admin_providers.py`**
1. **Point Rules CRUD** - Create, List, Update, Delete (soft-delete), Duplicate 409 - `ProviderUpdateInput` osztály: `is_always_open: Optional[bool] = Field(None, ...)` hozzáadva (148. sor)
2. **Level Configs CRUD** - Create, List, Update, Duplicate 409 - `update_provider()` PATCH endpoint: `is_always_open` kinyerése `update_fields`-ből, 0-24 kényszerítés True esetén, metadata törlés False esetén (1150-1170. sor)
3. **Badges CRUD** - Create, List, Update, Delete (hard-delete), Duplicate
4. **Seasons CRUD + Activation** - Create, List, Update, Activate, Single active verification
5. **Competitions CRUD** - Create, List, Update, Non-existent season 404
6. **User Stats** - List, Detail (404), Reward, Penalty, Update
7. **Master Config** - Get, Update, Verify
8. **System Params** - List, Update
9. **Points Ledger** - List, Pagination, Reason search, User filter
10. **Permission Check** - Regular user without gamification:manage gets 403
11. **Dashboard Summary** - Verify response structure
### ✅ Teszt Eredmények **2. Frontend - `frontend_admin/pages/providers/[id]/edit.vue`**
- **40/40 passed, 0 failed** ✅ - `form` reactive objektum: `is_always_open: false` mező hozzáadva
- Minden admin gamification végpont lefedve - Template: "Folyamatos (0-24) nyitvatartás" checkbox a Nyitvatartás tab tetején, amely letiltja a naponkénti grid-et
- Permission teszt: asyncpg-vel aktivált user, email verification-en keresztül - `onAlwaysOpenChange()`: Checkbox toggle handler - bekapcsoláskor `setAllDaysOpen('00:00', '23:59')`, kikapcsoláskor `clearAllHours()`
- Superadmin bypass token (`dev_bypass_active`) használata admin műveletekhez - `mapProviderToForm()`: `always_open` metadata felismerése a JSONB-ből (mind a régi flat, mind az új nested formátum támogatott)
- `hasChanges` computed: `is_always_open` összehasonlítás a `p.opening_hours?.always_open`-val
- `saveForm()`: `is_always_open: form.is_always_open` a PATCH payload-ban
- **Gitea #338:** ✅ Elkészült ### ✅ Verifikáció
- Backend Python szintaxis: OK
## 2026-06-29 - Gamification Nyelvi Modul Audit (#339) - Sync Engine: 1297 elem OK, 0 hiba, teljes szinkronban
- API Test 1: `PATCH is_always_open=True``{"always_open": true, "days": {"monday": {"open": "00:00", "close": "23:59"}, ...}}`
### 🎯 Cél - API Test 2: `PATCH is_always_open=False``{"monday": {"open": "09:00", "close": "17:00"}}` (always_open metadata stripped) ✅
A gamification backend és frontend admin modulok teljes körű nyelvi auditja - hardcoded stringek azonosítása és i18n javítási terv készítése.
### 🔧 Eredmények
- **16 fájl** átvizsgálva (3 backend + 12 frontend Vue + 1 nuxt.config)
- **Megállapítás:** A frontend admin 12 Vue oldalán 100%-ban hardcoded magyar nyelvű szövegek, sehol nincs `useI18n()` vagy `$t()` használat
- **Backend:** Kevert magyar/angol hardcoded hibaüzenetek, keménykódolt kvíz (magyar), achievement címek (angol)
- **Lokációs fájlok:** Egyetlen gamification kulcs sem található a `frontend_admin/locales/*.json` és `backend/static/locales/*.json` fájlokban
- **Javítási javaslat:** 7 fázisú terv a teljes i18n migrációhoz (15-20 fájl módosítás)
- **Dokumentáció:** `docs/gamification_i18n_audit_and_fix_proposal.md` - részletes audit és fix proposal
- **Gitea #339:** ✅ Audit elkészült
## 2026-06-29 - Gitea #340: Gamification i18n kulcsok felvétele (Fázis 1/7)
### 🎯 Cél
Gamification i18n kulcsok felvétele a `frontend_admin/locales/en.json` és `hu.json` fájlokba a `docs/gamification_i18n_audit_and_fix_proposal.md` dokumentációban definiált kulcs struktúra alapján.
### 🔧 Módosított fájlok
- `frontend_admin/locales/en.json` — Hozzáadva a `gamification` szekció 11 al-szekcióval (dashboard, badges, competitions, config, leaderboard, ledger, levels, parameters, point_rules, seasons, users)
- `frontend_admin/locales/hu.json` — Hozzáadva a `gamification` szekció magyar fordítással
### ✅ Eredmény
Mindkét JSON fájl validálva (Python json.load), szintaktikailag helyes. A gamification szótár teljes egészében elérhető mind angol, mind magyar nyelven.
- **Gitea #340:** ✅ Implementáció kész
## 2026-06-29 - Gitea #341: Fázis 2/7 - 12 Vue oldal átalakítása useI18n() és t() hívásokkal
### 🎯 Cél
A 12 gamification admin Vue oldal hardcoded magyar szövegeinek lecserélése `useI18n()` + `t()` hívásokra, a #340-ben felvett i18n kulcsok használatával.
### 🔧 Módosított fájlok
- `frontend_admin/i18n/locales/en.json` — Kiegészítve hiányzó gamification kulcsokkal (icon_hint, icon_preview, create_btn, unknown_error, name_placeholder, desc_placeholder, select_season, rules_placeholder, season_id_label)
- `frontend_admin/i18n/locales/hu.json` — Kiegészítve hiányzó gamification kulcsokkal (magyar fordítás)
- `frontend_admin/pages/gamification/badges.vue` — useI18n() + t() konverzió
- `frontend_admin/pages/gamification/competitions.vue` — useI18n() + t() konverzió
- `frontend_admin/pages/gamification/config.vue` — useI18n() + t() konverzió
- `frontend_admin/pages/gamification/index.vue` — useI18n() + t() konverzió
- `frontend_admin/pages/gamification/leaderboard.vue` — useI18n() + t() konverzió
- `frontend_admin/pages/gamification/ledger.vue` — useI18n() + t() konverzió
- `frontend_admin/pages/gamification/levels.vue` — useI18n() + t() konverzió
- `frontend_admin/pages/gamification/parameters.vue` — useI18n() + t() konverzió
- `frontend_admin/pages/gamification/point-rules.vue` — useI18n() + t() konverzió
- `frontend_admin/pages/gamification/seasons.vue` — useI18n() + t() konverzió
- `frontend_admin/pages/gamification/users/index.vue` — useI18n() + t() konverzió
- `frontend_admin/pages/gamification/users/[id].vue` — useI18n() + t() konverzió
### ✅ Eredmény
- Build: ✅ Sikeres (`✓ built in 6.95s`)
- Mind a 12 Vue oldal átalakítva: `const { t } = useI18n()` hozzáadva, minden hardcoded string `t('gamification.xxx.yyy')` hívással helyettesítve
- Interpoláció támogatás: delete confirmációk, penalty/reward sikerüzenetek, paraméter frissítés üzenetek
- **Gitea #341:** ✅ Elkészült
## 2026-06-29 - Debug: JSON Parse Error javítás (Gitea #341 utómunka)
### 🐛 Hiba
Nuxt runtime error: `[plugin json] i18n/locales/hu.json: Could not parse JSON file` — a build után a böngészőben JSON parse hiba lépett fel.
### 🔧 Javított hibák
1. **hu.json (line 377):** `'{"type": "service_submission", ...}'` — a JSON string értékeknél aposztróf (`'`) helyett dupla idézőjel (`"`) kell, escape-elve: `"{\"type\": \"service_submission\", ...}"`
2. **en.json (line 377):** Ugyanez a hiba — aposztróf használata JSON stringben
3. **hu.json (truncated):** A fájl csonkán íródott ki — hiányzott a `seasons`, `users`, `user_detail` szekció és a záró `}}}`
### ✅ Eredmény
- Mindkét JSON fájl validálva Python `json.load()`-dal: ✅ Sikeres
- Build: ✅ Sikeres (`✓ built in 6.88s`)
- Mind a 12 gamification i18n szekció elérhető mindkét nyelvben
## 2026-06-30 - [#342] Backend i18n helper megírása (app/core/i18n.py)
### 🎯 Cél
Backend i18n segédosztály létrehozása a `backend/static/locales/*.json` fájlok betöltésére és kulcs alapú szöveg lekérésre.
### 🔧 Módosított fájlok
- `backend/app/core/i18n.py` - Teljes átírás: JSON fájlokból cache-elt szótár építése, dot-notation kulcsfeloldás, `{{variable}}` interpoláció, context locale integráció, EN fallback, `reload_locales()` támogatás
### ✅ Eredmények
- `t(key, lang, **kwargs)` - Dot-notation kulcsfeloldás és `{{variable}}` interpoláció
- `get_locale(lang)` - Teljes nyelvi szótár visszaadása
- `reload_locales()` - Cache újratöltés futásidőben
- Locale detekció: explicit `lang` param → context locale (I18nMiddleware) → `"hu"` default
- Fallback lánc: requested lang → EN → bármely elérhető nyelv → kulcsnév
- 10/10 teszt sikeres (EN, HU, változók, fallback, context, reload)
## 2026-06-30 - [#345] Fázis 6/7: Kvíz tartalom kiszervezése JSON/DB-be nyelvi kulcsokkal
### 🎯 Cél
A keménykódolt magyar nyelvű kvíz kérdések, válaszok és magyarázatok kiszervezése a kódból nyelvi JSON fájlokba.
### 🔧 Módosított fájlok
- `backend/static/quiz/en.json` - **LÉTREHOZVA**: Angol nyelvű kvíz kérdések (3 db) JSON formátumban
- `backend/static/quiz/hu.json` - **LÉTREHOZVA**: Magyar nyelvű kvíz kérdések (3 db) JSON formátumban
- `backend/app/core/i18n.py` - **MÓDOSÍTVA**: `get_quiz_data()`, `reload_quiz_data()` függvények hozzáadva a kvíz JSON-ek cache-elt betöltéséhez
- `backend/app/api/v1/endpoints/gamification.py` - **MÓDOSÍTVA**: `GET /quiz/questions` és `POST /quiz/answer` végpontok hardcoded adatainak kiváltása `get_quiz_data()` hívásokkal
### ✅ Eredmények
- `get_quiz_data(lang)` - Kvíz kérdések betöltése nyelv szerinti JSON fájlokból, locale detekcióval (explicit lang → context locale → "hu" default → EN fallback)
- `reload_quiz_data()` - Kvíz cache újratöltés futásidőben
- 3 kvíz kérdés mindkét nyelven (HU + EN), minden opcióval és magyarázattal
- Szintaxis ellenőrzés: ✅ Sikeres
- Kvíz adatbetöltés teszt: ✅ Sikeres (HU: 3, EN: 3, DE fallback: 3)
## 2026-06-30 - [#346] Fázis 7/7: Tesztelés - magyar és angol nyelvű gamification felület
### 🎯 Cél
A gamification i18n migráció teljes körű tesztelése: backend API lokalizált hibaüzenetek, kvíz rendszer nyelvfüggő működése, achievement címek nyelvfüggő megjelenítése, nyelvváltás konzisztencia, hiányzó fordítás fallback viselkedés, frontend build ellenőrzés.
### 🔧 Eredmények
**1. BACKEND E2E TESZTEK (Admin Gamification):** ✅ 40/40 PASS
- Point Rules CRUD: ✅ 5/5
- Level Configs CRUD: ✅ 5/5
- Badges CRUD: ✅ 5/5
- Seasons CRUD: ✅ 5/5
- Competitions CRUD: ✅ 5/5
- User Stats: ✅ 4/4
- Master Config: ✅ 2/2
- System Params: ✅ 2/2
- Points Ledger: ✅ 2/2
- Permission Check: ✅ 3/3
- Dashboard Summary: ✅ 2/2
**2. BACKEND I18N API TESZTEK:** ✅ 15/15 PASS
- Localized Error Messages (HU/EN): ✅ 4/4
- Quiz Language-Dependent Operation: ✅ 3/3
- Achievement Titles Language Display: ✅ 1/1
- Language Switch Consistency: ✅ 4/4
- Missing Translation Fallback: ✅ 3/3
**3. FRONTEND ADMIN BUILD:** ✅ Sikeres
- Nuxt 3 build: 313 modules transformed, 7.96s client + 6.71s server
**4. FRONTEND I18N ELLENŐRZÉS:** ✅ 11/11 oldal
- Minden gamification admin oldal használja a `useI18n().t()` függvényt
- en.json (817 sor) és hu.json (719 sor) teljes gamification szekcióval
### 🔧 Módosított fájlok
- `backend/tests/e2e/test_gamification_i18n_api.py` - Login fix: `json=``data=` (OAuth2PasswordRequestForm kompatibilitás)

View File

@@ -7,7 +7,7 @@ from app.api.v1.endpoints import (
gamification, translations, users, reports, dictionaries, gamification, translations, users, reports, dictionaries,
admin_packages, admin_services, admin_organizations, admin_users, admin_persons, constants, providers, admin_packages, admin_services, admin_organizations, admin_users, admin_persons, constants, providers,
subscriptions, marketing, admin_permissions, regions, subscriptions, marketing, admin_permissions, regions,
admin_gamification, admin_gamification, admin_providers,
) )
api_router = APIRouter() api_router = APIRouter()
@@ -45,4 +45,5 @@ api_router.include_router(providers.router, prefix="/providers", tags=["Provider
api_router.include_router(subscriptions.router, prefix="/subscriptions", tags=["Subscriptions"]) api_router.include_router(subscriptions.router, prefix="/subscriptions", tags=["Subscriptions"])
api_router.include_router(marketing.router, prefix="/marketing", tags=["Marketing & Ads"]) api_router.include_router(marketing.router, prefix="/marketing", tags=["Marketing & Ads"])
api_router.include_router(admin_permissions.router, prefix="", tags=["Admin Permissions (RBAC)"]) api_router.include_router(admin_permissions.router, prefix="", tags=["Admin Permissions (RBAC)"])
api_router.include_router(admin_gamification.router, prefix="/admin/gamification", tags=["Admin Gamification"]) api_router.include_router(admin_gamification.router, prefix="/admin/gamification", tags=["Admin Gamification"])
api_router.include_router(admin_providers.router, prefix="/admin/providers", tags=["Admin Provider Moderation"])

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ from sqlalchemy import select, func, desc
from app.api.deps import get_db, get_current_user, RequireOrgCapability from app.api.deps import get_db, get_current_user, RequireOrgCapability
from app.models import Asset, AssetCost, AssetEvent, OrganizationMember, SystemParameter, OrgRole, CostCategory, Organization from app.models import Asset, AssetCost, AssetEvent, OrganizationMember, SystemParameter, OrgRole, CostCategory, Organization
from app.schemas.asset_cost import AssetCostCreate, AssetCostUpdate from app.schemas.asset_cost import AssetCostCreate, AssetCostUpdate
from app.services.provider_service import find_or_create_provider_by_name
from datetime import datetime, timezone from datetime import datetime, timezone
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -563,6 +564,32 @@ async def create_expense(
if expense.description: if expense.description:
data["description"] = expense.description data["description"] = expense.description
# ── PROVIDER AUTO-DISCOVERY HOOK (Card #360) ──
# Ha external_vendor_name meg van adva, de service_provider_id nincs,
# automatikusan felfedezzük vagy létrehozzuk a providert.
# A find_or_create_provider_by_name() kezeli a gamification pontok kiosztását is
# (PROVIDER_DISCOVERY / PROVIDER_CONFIRMATION / PROVIDER_VERIFIED_USE).
resolved_provider_id = expense.service_provider_id
if expense.external_vendor_name and not expense.service_provider_id:
try:
provider, action_key = await find_or_create_provider_by_name(
db=db,
name=expense.external_vendor_name,
added_by_user_id=current_user.id,
)
resolved_provider_id = provider.id
logger.info(
f"Provider auto-discovery: name='{expense.external_vendor_name}', "
f"provider_id={provider.id}, action_key='{action_key}', "
f"user_id={current_user.id}"
)
except Exception as e:
# Ha a provider felderítés hibázik, ne blokkolja a költség rögzítését
logger.warning(
f"Provider auto-discovery failed for '{expense.external_vendor_name}': {e}. "
f"Expense will be created without provider link."
)
try: try:
# Create AssetCost instance with new fields # Create AssetCost instance with new fields
new_cost = AssetCost( new_cost = AssetCost(
@@ -580,7 +607,8 @@ async def create_expense(
# === B2B VENDOR FIELDS === # === B2B VENDOR FIELDS ===
vendor_organization_id=expense.vendor_organization_id, vendor_organization_id=expense.vendor_organization_id,
# P0 HYBRID VENDOR REFACTOR: service_provider_id az expense creation-ben # P0 HYBRID VENDOR REFACTOR: service_provider_id az expense creation-ben
service_provider_id=expense.service_provider_id, # Ha az auto-discovery hook feloldotta a providert, a resolved_provider_id-t használjuk
service_provider_id=resolved_provider_id,
external_vendor_name=expense.external_vendor_name, external_vendor_name=expense.external_vendor_name,
# === INVOICE DATES === # === INVOICE DATES ===
invoice_date=expense.invoice_date, invoice_date=expense.invoice_date,

View File

@@ -36,8 +36,8 @@ async def register_service_hunt(
"""), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng, "user_id": current_user.id}) """), {"n": name, "f": f"{name}-{lat}-{lng}", "lat": lat, "lng": lng, "user_id": current_user.id})
# MB 2.0 Gamification: Dinamikus pontszám a felfedezésért # MB 2.0 Gamification: Dinamikus pontszám a felfedezésért
reward_points = await ConfigService.get_int(db, "GAMIFICATION_HUNT_REWARD", 50) # A pontérték a point_rules táblából jön (action_key='SERVICE_HUNT')
await GamificationService.award_points(db, current_user.id, reward_points, f"Service Hunt: {name}") await GamificationService.award_points(db, current_user.id, amount=0, reason=f"Service Hunt: {name}", action_key="SERVICE_HUNT")
await db.commit() await db.commit()
return {"status": "success", "message": "Discovery registered and points awarded."} return {"status": "success", "message": "Discovery registered and points awarded."}
@@ -86,8 +86,8 @@ async def validate_staged_service(
) )
# 5. Adományozz dinamikus XP-t a current_user-nek a GamificationService-en keresztül # 5. Adományozz dinamikus XP-t a current_user-nek a GamificationService-en keresztül
validation_reward = await ConfigService.get_int(db, "GAMIFICATION_VALIDATE_REWARD", 10) # A pontérték a point_rules táblából jön (action_key='SERVICE_VALIDATION')
await GamificationService.award_points(db, current_user.id, validation_reward, f"Service Validation: staging #{staging_id}") await GamificationService.award_points(db, current_user.id, amount=0, reason=f"Service Validation: staging #{staging_id}", action_key="SERVICE_VALIDATION")
# 6. Növeld a current_user places_validated értékét a UserStats-ban # 6. Növeld a current_user places_validated értékét a UserStats-ban
await db.execute( await db.execute(

View File

@@ -327,6 +327,23 @@ def reload_quiz_data() -> None:
logger.info("Quiz: Quiz data reloaded successfully") logger.info("Quiz: Quiz data reloaded successfully")
# ── LocaleManager wrapper for email_manager compatibility ──────────────
class _LocaleManager:
"""
Wrapper providing a ``.get()`` interface around the ``t()`` function.
The ``email_manager`` module imports ``locale_manager`` and calls
``locale_manager.get(key, lang=lang, **variables)``. This class
delegates to the existing ``t()`` function so that no import breaks.
"""
@staticmethod
def get(key: str, lang: Optional[str] = None, **kwargs: Any) -> str:
return t(key, lang=lang, **kwargs)
locale_manager = _LocaleManager()
# ── Preload on import ────────────────────────────────────────────────── # ── Preload on import ──────────────────────────────────────────────────
_load_locales() _load_locales()
_load_quiz_data() _load_quiz_data()

View File

@@ -48,6 +48,13 @@ class PointsLedger(Base):
source_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) source_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
source_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) source_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
# SNAPSHOT MECHANIZMUS (2026-06-30, #369)
# Tárolja a kiosztáskor érvényes point_rules adatokat, hogy a pontértékek
# megőrződjenek a ledger-ben, még akkor is, ha a point_rules táblában
# később módosítják a pontértékeket.
# Tartalma: {"action_key": str, "point_rule_id": int, "points_at_time": int, "multiplier": float}
points_snapshot: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User") user: Mapped["User"] = relationship("User")

View File

@@ -30,6 +30,7 @@ from .social import (
ServiceReview, ServiceReview,
ModerationStatus, ModerationStatus,
SourceType, SourceType,
ProviderValidation,
) )
__all__ = [ __all__ = [
@@ -58,4 +59,5 @@ __all__ = [
"ServiceReview", "ServiceReview",
"ModerationStatus", "ModerationStatus",
"SourceType", "SourceType",
"ProviderValidation",
] ]

View File

@@ -5,7 +5,7 @@ from datetime import datetime
from typing import Optional, List from typing import Optional, List
from sqlalchemy import String, Integer, ForeignKey, DateTime, Boolean, Text, UniqueConstraint, text from sqlalchemy import String, Integer, ForeignKey, DateTime, Boolean, Text, UniqueConstraint, text
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID, JSONB, ARRAY
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.database import Base from app.database import Base
@@ -17,6 +17,7 @@ class ModerationStatus(str, enum.Enum):
class SourceType(str, enum.Enum): class SourceType(str, enum.Enum):
manual = "manual" manual = "manual"
ocr = "ocr" ocr = "ocr"
api = "api"
api_import = "import" api_import = "import"
class ServiceProvider(Base): class ServiceProvider(Base):
@@ -26,7 +27,7 @@ class ServiceProvider(Base):
kapcsolatfelvételi adatokkal a quick_add_provider() refactorhoz. kapcsolatfelvételi adatokkal a quick_add_provider() refactorhoz.
""" """
__tablename__ = "service_providers" __tablename__ = "service_providers"
__table_args__ = {"schema": "marketplace"} __table_args__ = {"schema": "marketplace", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String, nullable=False) name: Mapped[str] = mapped_column(String, nullable=False)
@@ -58,6 +59,15 @@ class ServiceProvider(Base):
validation_score: Mapped[int] = mapped_column(Integer, default=0) validation_score: Mapped[int] = mapped_column(Integer, default=0)
evidence_image_path: Mapped[Optional[str]] = mapped_column(String) evidence_image_path: Mapped[Optional[str]] = mapped_column(String)
added_by_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id")) added_by_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
# === P0 3D FILTERING MATRIX: Járműosztályok és specializációk ===
supported_vehicle_classes: Mapped[Optional[list]] = mapped_column(
ARRAY(String), server_default=text("'{}'"), nullable=True
)
specializations: Mapped[Optional[dict]] = mapped_column(
JSONB, server_default=text("'{}'::jsonb"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class Vote(Base): class Vote(Base):
@@ -73,6 +83,45 @@ class Vote(Base):
provider_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_providers.id"), nullable=False) provider_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_providers.id"), nullable=False)
vote_value: Mapped[int] = mapped_column(Integer, nullable=False) # +1 vagy -1 vote_value: Mapped[int] = mapped_column(Integer, nullable=False) # +1 vagy -1
class ProviderValidation(Base):
"""
Provider validációs rekordok (XP farming prevention).
Minden egyes admin moderációs akció (approve/reject/flag) egy rekordot hoz létre
ebben a táblában. A Vote táblával ellentétben itt az admin által végzett
validációk kerülnek rögzítésre, súlyozott értékkel.
Features:
- UniqueConstraint(voter_user_id, provider_id): egy user csak egyszer validálhat
- weight: az admin szintjétől függő súly (pl. superadmin=10, admin=5)
- metadata: JSONB a validáció részleteivel (reason, evidence, stb.)
- Küszöbérték: ha a weight-ek összege >= VALIDATION_THRESHOLD, a provider auto-approved
"""
__tablename__ = "provider_validations"
__table_args__ = (
UniqueConstraint('voter_user_id', 'provider_id', name='uq_voter_provider_validation'),
{"schema": "marketplace", "extend_existing": True}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
provider_id: Mapped[int] = mapped_column(
Integer, ForeignKey("marketplace.service_providers.id", ondelete="CASCADE"), nullable=False, index=True
)
voter_user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("identity.users.id", ondelete="CASCADE"), nullable=False
)
validation_type: Mapped[str] = mapped_column(
String(20), nullable=False, default="approve"
) # approve | reject | flag
weight: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
validation_metadata: Mapped[Optional[dict]] = mapped_column("metadata", JSONB, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Relationships
provider: Mapped["ServiceProvider"] = relationship("ServiceProvider", foreign_keys=[provider_id])
voter: Mapped["User"] = relationship("User", foreign_keys=[voter_user_id])
class Competition(Base): class Competition(Base):
""" Gamifikált versenyek (pl. Januári Feltöltő Verseny). """ """ Gamifikált versenyek (pl. Januári Feltöltő Verseny). """
__tablename__ = "competitions" __tablename__ = "competitions"

View File

@@ -5,7 +5,7 @@ from datetime import datetime
from typing import Any, List, Optional from typing import Any, List, Optional
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, text, Text, Float, Index, Numeric, BigInteger from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, text, Text, Float, Index, Numeric, BigInteger
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB, ENUM as SQLEnum from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB, ENUM as SQLEnum, ARRAY
from geoalchemy2 import Geometry from geoalchemy2 import Geometry
from sqlalchemy.sql import func from sqlalchemy.sql import func
@@ -72,6 +72,14 @@ class ServiceProfile(Base):
website: Mapped[Optional[str]] = mapped_column(String) website: Mapped[Optional[str]] = mapped_column(String)
bio: Mapped[Optional[str]] = mapped_column(Text) bio: Mapped[Optional[str]] = mapped_column(Text)
# === P0 3D FILTERING MATRIX: Járműosztályok és specializációk ===
supported_vehicle_classes: Mapped[Optional[list]] = mapped_column(
ARRAY(String), server_default=text("'{}'"), nullable=True
)
specializations: Mapped[Optional[dict]] = mapped_column(
JSONB, server_default=text("'{}'::jsonb"), nullable=True
)
# Kapcsolatok # Kapcsolatok
organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile") organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile")
expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service") expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service")

View File

@@ -120,6 +120,8 @@ class ProviderSearchResult(BaseModel):
source: str = Field(..., description="Forrás: verified_org | staged_data | crowd_added") source: str = Field(..., description="Forrás: verified_org | staged_data | crowd_added")
is_verified: bool = False is_verified: bool = False
rating: Optional[float] = None rating: Optional[float] = None
supported_vehicle_classes: Optional[List[str]] = None
specializations: Optional[dict] = None
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@@ -159,6 +161,12 @@ class ProviderQuickAddIn(BaseModel):
contact_email: Optional[str] = Field(None, max_length=255, description="Email cím") contact_email: Optional[str] = Field(None, max_length=255, description="Email cím")
website: Optional[str] = Field(None, max_length=255, description="Weboldal URL") website: Optional[str] = Field(None, max_length=255, description="Weboldal URL")
tags: Optional[List[str]] = Field(None, description="Szolgáltatás címkék (specialization_tags)") tags: Optional[List[str]] = Field(None, description="Szolgáltatás címkék (specialization_tags)")
supported_vehicle_classes: Optional[List[str]] = Field(
None, description="Támogatott járműosztályok (pl. ['car', 'hgv', 'van'])"
)
specializations: Optional[dict] = Field(
None, description="Specializációk (pl. {'brands': ['Volvo'], 'propulsion': ['EV']})"
)
class ProviderQuickAddResponse(BaseModel): class ProviderQuickAddResponse(BaseModel):
@@ -198,6 +206,12 @@ class ProviderUpdateIn(BaseModel):
new_tags: Optional[List[str]] = Field( new_tags: Optional[List[str]] = Field(
None, description="User által gépelt új címkenevek (is_official=False, level=3)" None, description="User által gépelt új címkenevek (is_official=False, level=3)"
) )
supported_vehicle_classes: Optional[List[str]] = Field(
None, description="Támogatott járműosztályok (pl. ['car', 'hgv', 'van'])"
)
specializations: Optional[dict] = Field(
None, description="Specializációk (pl. {'brands': ['Volvo'], 'propulsion': ['EV']})"
)
class ProviderUpdateResponse(BaseModel): class ProviderUpdateResponse(BaseModel):

View File

@@ -0,0 +1,53 @@
"""
Check if provider_validations table exists and create if not.
"""
import asyncio
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
from app.core.config import settings
async def main():
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI))
async with engine.connect() as conn:
# Check if table exists
result = await conn.execute(
text("SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'marketplace' AND table_name = 'provider_validations')")
)
exists = result.scalar()
print(f"provider_validations table exists: {exists}")
if not exists:
# Create the table
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS marketplace.provider_validations (
id SERIAL PRIMARY KEY,
provider_id INTEGER NOT NULL REFERENCES marketplace.service_providers(id) ON DELETE CASCADE,
voter_user_id INTEGER NOT NULL REFERENCES identity.users(id) ON DELETE CASCADE,
validation_type VARCHAR(20) NOT NULL DEFAULT 'approve',
weight INTEGER NOT NULL DEFAULT 1,
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(voter_user_id, provider_id)
)
"""))
await conn.commit()
print("Created provider_validations table")
# Check columns
result = await conn.execute(text("""
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'marketplace' AND table_name = 'provider_validations'
ORDER BY ordinal_position
"""))
print("\nColumns:")
for row in result:
print(f" {row.column_name}: {row.data_type} (nullable: {row.is_nullable})")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,157 @@
"""
Gamification Action Keys Seed Script
=====================================
Cél: Hiányzó action_key-ek felvétele a gamification.point_rules táblába.
Az audit során az alábbi akciókhoz NEM létezik action_key a point_rules táblában:
- KYC_VERIFICATION (auth_service KYC completion)
- P2P_REFERRAL_SUCCESS (auth_service P2P referral)
- EXPENSE_LOG (cost_service expense recording)
- FLEET_EVENT (fleet_service vehicle event)
- PROVIDER_VALIDATED (social_service provider validation)
- PROVIDER_REJECTED (social_service provider rejection)
- SERVICE_HUNT (services.py service hunt)
- SERVICE_VALIDATION (services.py service validation)
Használat:
docker compose exec sf_api python3 /app/backend/app/scripts/seed_gamification_action_keys.py
"""
import asyncio
import logging
import sys
from pathlib import Path
# Projekt gyökér hozzáadása a Python path-hoz
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from app.core.config import settings
from app.models.gamification.gamification import PointRule
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("Seed-Gamification-Action-Keys")
# Hiányzó action_key-ek a point_rules táblába
# Pontértékek a meglévő ConfigService default értékek alapján:
# KYC_VERIFICATION = 100 (auth_service: kyc_reward)
# P2P_REFERRAL_SUCCESS = 50 (auth_service: gamification_p2p_invite_xp default)
# EXPENSE_LOG = 50 (cost_service: xp_per_cost_log default)
# FLEET_EVENT = 20 (fleet_service: event_rewards["default"]["xp"])
# PROVIDER_VALIDATED = 100 (social_service: hardcoded 100XP)
# PROVIDER_REJECTED = 50 (social_service: hardcoded 50XP)
# SERVICE_HUNT = 50 (services.py: GAMIFICATION_HUNT_REWARD default)
# SERVICE_VALIDATION = 10 (services.py: GAMIFICATION_VALIDATE_REWARD default)
SEED_RULES = [
{
"action_key": "KYC_VERIFICATION",
"points": 100,
"description": "Sikeres KYC (Know Your Customer) folyamat teljesítése",
"is_active": True,
},
{
"action_key": "P2P_REFERRAL_SUCCESS",
"points": 50,
"description": "Sikeres P2P meghívó (referred_by_id alapján)",
"is_active": True,
},
{
"action_key": "EXPENSE_LOG",
"points": 50,
"description": "Költség rögzítése (base_xp, OCR bónusz nélkül)",
"is_active": True,
},
{
"action_key": "FLEET_EVENT",
"points": 20,
"description": "Flotta esemény rögzítése (alapértelmezett pont)",
"is_active": True,
},
{
"action_key": "PROVIDER_VALIDATED",
"points": 100,
"description": "Provider adatainak közösségi validálása (jóváhagyás)",
"is_active": True,
},
{
"action_key": "PROVIDER_REJECTED",
"points": 50,
"description": "Provider adatainak közösségi elutasítása (büntetés)",
"is_active": True,
},
{
"action_key": "SERVICE_HUNT",
"points": 50,
"description": "Új szerviz felfedezése (service hunt)",
"is_active": True,
},
{
"action_key": "SERVICE_VALIDATION",
"points": 10,
"description": "Szerviz validálása (más user által beküldött staging rekord)",
"is_active": True,
},
]
async def seed_gamification_action_keys():
"""
Feltölti a gamification.point_rules táblát a hiányzó action_key-ekkel.
Csak azokat a rekordokat szúrja be, amelyek még nem léteznek (action_key alapján).
"""
engine = create_async_engine(settings.DATABASE_URL, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as db:
try:
inserted_count = 0
skipped_count = 0
for rule_data in SEED_RULES:
# Ellenőrizzük, hogy létezik-e már ez a szabály
stmt = select(PointRule).where(
PointRule.action_key == rule_data["action_key"]
)
result = await db.execute(stmt)
existing = result.scalar_one_or_none()
if existing:
logger.info(
f"⏭️ Szabály már létezik: {rule_data['action_key']} "
f"(pont: {existing.points}, ID: {existing.id})"
)
skipped_count += 1
continue
# Új szabály beszúrása
new_rule = PointRule(
action_key=rule_data["action_key"],
points=rule_data["points"],
description=rule_data["description"],
is_active=rule_data["is_active"],
)
db.add(new_rule)
await db.flush()
logger.info(
f"✅ Szabály létrehozva: {rule_data['action_key']} "
f"(pont: {rule_data['points']}, ID: {new_rule.id})"
)
inserted_count += 1
await db.commit()
logger.info(
f"\n📊 Összegzés: {inserted_count} új szabály beszúrva, "
f"{skipped_count} már létezett."
)
except Exception as e:
await db.rollback()
logger.error(f"❌ Hiba a seedelés során: {e}", exc_info=True)
raise
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(seed_gamification_action_keys())

View File

@@ -0,0 +1,124 @@
"""
Provider Gamification Point Rules Seed Script
=============================================
Cél: Új gamification pontszabályok beszúrása a provider discovery rendszerhez.
Új szabályok:
- PROVIDER_DISCOVERY (300 pont) — Új provider felfedezése expense rögzítéskor
- PROVIDER_CONFIRMATION (150 pont) — Meglévő provider adatainak megerősítése
- PROVIDER_VERIFIED_USE (100 pont) — Már validált provider használata
Használat:
docker compose exec sf_api python3 /app/backend/app/scripts/seed_provider_point_rules.py
"""
import asyncio
import logging
import sys
from pathlib import Path
# Projekt gyökér hozzáadása a Python path-hoz
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from app.core.config import settings
from app.models.gamification.gamification import PointRule
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("Seed-Provider-Point-Rules")
# Új provider discovery pontszabályok
# A pontértékek a kártya specifikációja szerint (#356, #362):
# PROVIDER_DISCOVERY = 300 — Új provider felfedezése
# PROVIDER_CONFIRMATION = 150 — Saját pending provider használata
# PROVIDER_VERIFIED_USE = 100 — Már validált provider használata
# USE_UNVERIFIED_PROVIDER = 200 — Más user által létrehozott, még pending provider használata
SEED_RULES = [
{
"action_key": "PROVIDER_DISCOVERY",
"points": 300,
"description": "Új, még nem létező provider felfedezése expense rögzítéskor (external_vendor_name alapján)",
"is_active": True,
},
{
"action_key": "PROVIDER_CONFIRMATION",
"points": 150,
"description": "Saját magad által létrehozott, még pending státuszú provider használata",
"is_active": True,
},
{
"action_key": "PROVIDER_VERIFIED_USE",
"points": 100,
"description": "Már jóváhagyott (approved) provider használata költség rögzítésnél",
"is_active": True,
},
{
"action_key": "USE_UNVERIFIED_PROVIDER",
"points": 200,
"description": "Más user által létrehozott, még pending státuszú provider használata (XP farming prevention)",
"is_active": True,
},
]
async def seed_provider_point_rules():
"""
Feltölti a gamification.point_rules táblát a provider discovery szabályokkal.
Csak azokat a rekordokat szúrja be, amelyek még nem léteznek (action_key alapján).
"""
engine = create_async_engine(settings.DATABASE_URL, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as db:
try:
inserted_count = 0
skipped_count = 0
for rule_data in SEED_RULES:
# Ellenőrizzük, hogy létezik-e már ez a szabály
stmt = select(PointRule).where(
PointRule.action_key == rule_data["action_key"]
)
result = await db.execute(stmt)
existing = result.scalar_one_or_none()
if existing:
logger.info(
f"⏭️ Szabály már létezik: {rule_data['action_key']} "
f"(pont: {existing.points}, ID: {existing.id})"
)
skipped_count += 1
continue
# Új szabály beszúrása
new_rule = PointRule(
action_key=rule_data["action_key"],
points=rule_data["points"],
description=rule_data["description"],
is_active=rule_data["is_active"],
)
db.add(new_rule)
await db.flush()
logger.info(
f"✅ Szabály létrehozva: {rule_data['action_key']} "
f"(pont: {rule_data['points']}, ID: {new_rule.id})"
)
inserted_count += 1
await db.commit()
logger.info(
f"\n📊 Összegzés: {inserted_count} új szabály beszúrva, "
f"{skipped_count} már létezett."
)
except Exception as e:
await db.rollback()
logger.error(f"❌ Hiba a seedelés során: {e}", exc_info=True)
raise
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(seed_provider_point_rules())

View File

@@ -286,8 +286,8 @@ class AssetService:
)) ))
# Gamification # Gamification
reward = await config.get_setting(db, "xp_reward_asset_register", default=250) # A pontérték a point_rules táblából jön (action_key='ASSET_REGISTER')
await GamificationService.award_points(db, user_id, int(reward), "NEW_ASSET_REG") await GamificationService.award_points(db, user_id, amount=0, reason="ASSET_REGISTER", action_key="ASSET_REGISTER")
# Check if this is user's first vehicle and award "First Car" badge # Check if this is user's first vehicle and award "First Car" badge
await AssetService._award_first_car_badge(db, user_id, target_org_id) await AssetService._award_first_car_badge(db, user_id, target_org_id)

View File

@@ -295,19 +295,21 @@ class AuthService:
user.region_code = kyc_in.region_code user.region_code = kyc_in.region_code
# Gamification XP jóváírás (commit=False, mert a külső metódus kezeli a tranzakciót) # Gamification XP jóváírás (commit=False, mert a külső metódus kezeli a tranzakciót)
await GamificationService.award_points(db, user_id=user.id, amount=int(kyc_reward), reason="KYC_VERIFICATION", commit=False) # A pontérték a point_rules táblából jön (action_key='KYC_VERIFICATION')
await GamificationService.award_points(db, user_id=user.id, amount=0, reason="KYC_VERIFICATION", commit=False, action_key="KYC_VERIFICATION")
# P2P Referral XP a meghívónak (ha van referred_by_id) # P2P Referral XP a meghívónak (ha van referred_by_id)
if user.referred_by_id: if user.referred_by_id:
p2p_xp = await config.get_setting(db, "gamification_p2p_invite_xp", default=50) # A pontérték a point_rules táblából jön (action_key='P2P_REFERRAL_SUCCESS')
await GamificationService.award_points( await GamificationService.award_points(
db, db,
user_id=user.referred_by_id, user_id=user.referred_by_id,
amount=int(p2p_xp), amount=0,
reason="P2P_REFERRAL_SUCCESS", reason="P2P_REFERRAL_SUCCESS",
commit=False commit=False,
action_key="P2P_REFERRAL_SUCCESS"
) )
logger.info(f"P2P XP ({p2p_xp}) awarded to referrer ID {user.referred_by_id} for user {user.id}") logger.info(f"P2P XP awarded to referrer ID {user.referred_by_id} for user {user.id}")
await db.commit() await db.commit()
return user return user

View File

@@ -25,8 +25,6 @@ class CostService:
try: try:
# 1. Dinamikus konfiguráció lekérése # 1. Dinamikus konfiguráció lekérése
base_currency = await config.get_setting(db, "finance_base_currency", default="EUR") base_currency = await config.get_setting(db, "finance_base_currency", default="EUR")
base_xp = await config.get_setting(db, "xp_per_cost_log", default=50)
ocr_multiplier = await config.get_setting(db, "xp_multiplier_ocr_cost", default=1.5)
# 2. Intelligens Árfolyamkezelés # 2. Intelligens Árfolyamkezelés
exchange_rate = Decimal("1.0") exchange_rate = Decimal("1.0")
@@ -66,12 +64,10 @@ class CostService:
await self._sync_telemetry(db, cost_in.asset_id, cost_in.mileage_at_cost) await self._sync_telemetry(db, cost_in.asset_id, cost_in.mileage_at_cost)
# 5. Gamification (Értékesebb az adat, ha van róla fotó/OCR) # 5. Gamification (Értékesebb az adat, ha van róla fotó/OCR)
final_xp = base_xp # A pontérték a point_rules táblából jön (action_key='EXPENSE_LOG')
if new_cost.is_ai_generated: # Az OCR bónusz továbbra is érvényesül a process_activity szorzóin keresztül
final_xp = int(base_xp * float(ocr_multiplier))
await GamificationService.award_points( await GamificationService.award_points(
db, user_id=user_id, amount=final_xp, reason=f"EXPENSE_LOG_{cost_in.cost_type}" db, user_id=user_id, amount=0, reason=f"EXPENSE_LOG_{cost_in.cost_type}", action_key="EXPENSE_LOG"
) )
await db.commit() await db.commit()

View File

@@ -82,15 +82,17 @@ class FleetService:
db.add(new_event) db.add(new_event)
# 6. DINAMIKUS GAMIFIKÁCIÓ # 6. DINAMIKUS GAMIFIKÁCIÓ
# Kikeresjük a konkrét eseménytípushoz tartozó pontokat # A pontérték a point_rules táblából jön (action_key='FLEET_EVENT')
# A social pont továbbra is az event_rewards konfigurációból jön
rewards = event_rewards.get(event_data.event_type, event_rewards["default"]) rewards = event_rewards.get(event_data.event_type, event_rewards["default"])
await gamification_service.process_activity( await gamification_service.process_activity(
db, db,
user_id, user_id,
xp_amount=rewards["xp"], xp_amount=0,
social_amount=rewards["social"], social_amount=rewards["social"],
reason=f"FLEET_EVENT_{event_data.event_type.upper()}" reason=f"FLEET_EVENT_{event_data.event_type.upper()}",
action_key="FLEET_EVENT"
) )
await db.commit() await db.commit()

View File

@@ -1,6 +1,7 @@
# /opt/docker/dev/service_finder/backend/app/services/gamification_service.py # /opt/docker/dev/service_finder/backend/app/services/gamification_service.py
import logging import logging
import math import math
from copy import deepcopy
from decimal import Decimal from decimal import Decimal
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc from sqlalchemy import select, desc
@@ -12,6 +13,25 @@ from app.services.config_service import config # 2.0 Központi konfigurátor
logger = logging.getLogger("Gamification-Service-2.0") logger = logging.getLogger("Gamification-Service-2.0")
def _deep_merge_config(base: dict, overlay: dict) -> dict:
"""
Rekurzívan egyesít két dictionary-t.
Az overlay kulcsai felülírják a base kulcsait, de a base-ben lévő
hiányzó kulcsok megmaradnak az overlay-ből.
Ez biztosítja, hogy a GAMIFICATION_MASTER_CONFIG minden szükséges
kulcsot tartalmazzon, még akkor is, ha az adatbázisban lévő konfiguráció
hiányos (pl. régebbi verzióból származik).
"""
result = deepcopy(base)
for key, value in overlay.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = _deep_merge_config(result[key], value)
elif key not in result:
result[key] = deepcopy(value)
return result
class GamificationService: class GamificationService:
""" """
Gamification Service 2.0 - A 'Jövevény' lelke. Gamification Service 2.0 - A 'Jövevény' lelke.
@@ -22,22 +42,24 @@ class GamificationService:
""" """
@staticmethod @staticmethod
async def award_points(db: AsyncSession, user_id: int, amount: int, reason: str, social_points: int = 0, commit: bool = True): async def award_points(db: AsyncSession, user_id: int, amount: int, reason: str, social_points: int = 0, commit: bool = True, action_key: str | None = None):
""" Statikus segédfüggvény a Robotok számára az egyszerűbb híváshoz. """ Statikus segédfüggvény a Robotok számára az egyszerűbb híváshoz.
Args: Args:
commit: If True (default), commits the transaction internally. commit: If True (default), commits the transaction internally.
Set to False when called from within a larger transaction Set to False when called from within a larger transaction
(e.g., from AuthService.complete_kyc). (e.g., from AuthService.complete_kyc).
action_key: Opcionális. Ha meg van adva, a point_rules táblából
olvassa a pontértékeket a master config helyett.
""" """
service = GamificationService() service = GamificationService()
return await service.process_activity(db, user_id, xp_amount=amount, social_amount=social_points, reason=reason, commit=commit) return await service.process_activity(db, user_id, xp_amount=amount, social_amount=social_points, reason=reason, commit=commit, action_key=action_key)
async def _get_point_rule(self, db: AsyncSession, action_key: str) -> dict | None: async def _get_point_rule(self, db: AsyncSession, action_key: str) -> dict | None:
"""Lekér egy pontszabályt a point_rules táblából action_key alapján. """Lekér egy pontszabályt a point_rules táblából action_key alapján.
Returns: Returns:
dict with 'points' and 'description' or None if not found/inactive. dict with 'points', 'description', 'rule_id' or None if not found/inactive.
""" """
stmt = select(PointRule).where( stmt = select(PointRule).where(
PointRule.action_key == action_key, PointRule.action_key == action_key,
@@ -46,7 +68,7 @@ class GamificationService:
result = await db.execute(stmt) result = await db.execute(stmt)
rule = result.scalar_one_or_none() rule = result.scalar_one_or_none()
if rule: if rule:
return {"points": rule.points, "description": rule.description} return {"points": rule.points, "description": rule.description, "rule_id": rule.id}
return None return None
async def process_activity( async def process_activity(
@@ -80,7 +102,7 @@ class GamificationService:
try: try:
# 1. ADMIN KONFIGURÁCIÓ BETÖLTÉSE # 1. ADMIN KONFIGURÁCIÓ BETÖLTÉSE
# Minden paraméter az admin felületről módosítható JSON-ként # Minden paraméter az admin felületről módosítható JSON-ként
cfg = await config.get_setting(db, "GAMIFICATION_MASTER_CONFIG", default={ _DEFAULT_GAMIFICATION_CONFIG = {
"xp_logic": {"base_xp": 500, "exponent": 1.5}, "xp_logic": {"base_xp": 500, "exponent": 1.5},
"penalty_logic": { "penalty_logic": {
"recovery_rate": 0.5, "recovery_rate": 0.5,
@@ -89,15 +111,28 @@ class GamificationService:
}, },
"conversion_logic": {"social_to_credit_rate": 100}, "conversion_logic": {"social_to_credit_rate": 100},
"level_rewards": {"credits_per_10_levels": 50} "level_rewards": {"credits_per_10_levels": 50}
}) }
cfg = await config.get_setting(db, "GAMIFICATION_MASTER_CONFIG", default=_DEFAULT_GAMIFICATION_CONFIG)
# 🔐 HIÁNYZÓ KULCSOK VÉDELME (Deep Merge)
# Ha a GAMIFICATION_MASTER_CONFIG már létezik az adatbázisban, de hiányoznak
# belőle kulcsok (pl. penalty_logic egy régebbi verzióból), a default értékek
# automatikusan kiegészítik a hiányzó részeket.
# Ez megakadályozza a KeyError kivételeket a cfg["penalty_logic"] hívásoknál.
if isinstance(cfg, dict):
cfg = _deep_merge_config(cfg, _DEFAULT_GAMIFICATION_CONFIG)
# 1/b. POINT RULES TÁBLA LEKÉRÉSE (ha action_key meg van adva) # 1/b. POINT RULES TÁBLA LEKÉRÉSE (ha action_key meg van adva)
# A point_rules tábla elsőbbséget élvez a master config-gal szemben! # A point_rules tábla elsőbbséget élvez a master config-gal szemben!
point_rule_id = None
points_at_time = None
if action_key: if action_key:
rule = await self._get_point_rule(db, action_key) rule = await self._get_point_rule(db, action_key)
if rule: if rule:
# A point_rules-ból jövő pontok felülírják a paraméterként kapott értékeket # A point_rules-ból jövő pontok felülírják a paraméterként kapott értékeket
xp_amount = rule["points"] xp_amount = rule["points"]
point_rule_id = rule.get("rule_id")
points_at_time = rule["points"]
if rule.get("description"): if rule.get("description"):
reason = f"{action_key}: {rule['description']}" reason = f"{action_key}: {rule['description']}"
logger.debug(f"Point rule applied: {action_key} -> {xp_amount} XP") logger.debug(f"Point rule applied: {action_key} -> {xp_amount} XP")
@@ -152,7 +187,18 @@ class GamificationService:
stats.social_points %= rate # A maradék pont megmarad stats.social_points %= rate # A maradék pont megmarad
await self._add_earned_credits(db, user_id, credits_to_add, "SOCIAL_ACTIVITY_CONVERSION") await self._add_earned_credits(db, user_id, credits_to_add, "SOCIAL_ACTIVITY_CONVERSION")
# 7. NAPLÓZÁS (kibővítve xp, source_type, source_id mezőkkel) # 7. NAPLÓZÁS (kibővítve xp, source_type, source_id, points_snapshot mezőkkel)
# A points_snapshot JSONB tárolja a kiosztáskor érvényes point_rules adatokat,
# hogy a pontértékek megőrződjenek a ledger-ben, még akkor is, ha a
# point_rules táblában később módosítják a pontértékeket.
snapshot = None
if action_key and point_rule_id and points_at_time is not None:
snapshot = {
"action_key": action_key,
"point_rule_id": point_rule_id,
"points_at_time": points_at_time,
"multiplier": multiplier,
}
db.add(PointsLedger( db.add(PointsLedger(
user_id=user_id, user_id=user_id,
points=final_xp, points=final_xp,
@@ -160,6 +206,7 @@ class GamificationService:
reason=reason, reason=reason,
source_type=source_type, source_type=source_type,
source_id=source_id, source_id=source_id,
points_snapshot=snapshot,
)) ))
# Only commit if caller wants us to manage the transaction # Only commit if caller wants us to manage the transaction

View File

@@ -40,7 +40,7 @@ from sqlalchemy.orm import joinedload
from app.models.marketplace.organization import Organization, OrgType, Branch, OrganizationMember, OrgUserRole from app.models.marketplace.organization import Organization, OrgType, Branch, OrganizationMember, OrgUserRole
from app.models.marketplace.service import ServiceProfile, ServiceStatus, ExpertiseTag, ServiceExpertise, ServiceStaging from app.models.marketplace.service import ServiceProfile, ServiceStatus, ExpertiseTag, ServiceExpertise, ServiceStaging
from app.models.identity.social import ServiceProvider from app.models.identity.social import ServiceProvider, ModerationStatus, SourceType
from app.models.gamification.gamification import PointRule, UserStats from app.models.gamification.gamification import PointRule, UserStats
from app.schemas.provider import ( from app.schemas.provider import (
ProviderSearchResult, ProviderSearchResult,
@@ -64,9 +64,12 @@ async def _award_provider_points(
""" """
Dinamikus pontjóváírás a gamification.point_rules tábla alapján. Dinamikus pontjóváírás a gamification.point_rules tábla alapján.
Ez a függvény az adatbázisból olvassa ki a pontértéket az action_key REFAKTOR (2026-06-30): A duplikált point_rules olvasási logika eltávolításra
alapján, így a pontok NINCSENEK beégetve a kódba. Az admin felületről került. A pontértékeket most a GamificationService.award_points() olvassa ki
bármikor módosíthatók a point_rules táblában. a point_rules táblából az action_key alapján a process_activity() hívás során.
Ez a függvény már csak a providers_added_count statisztikát kezeli.
A pontok kiszámítása és naplózása a GamificationService-ben történik.
Args: Args:
db: AsyncSession db: AsyncSession
@@ -76,39 +79,19 @@ async def _award_provider_points(
Returns: Returns:
int: A jóváírt pontok száma (0 ha a szabály nem található vagy inaktív) int: A jóváírt pontok száma (0 ha a szabály nem található vagy inaktív)
""" """
# 1. Pontszabály lekérdezése az adatbázisból (dinamikus!) # 1. Pontok jóváírása a Gamification Service-en keresztül (action_key alapján)
rule_stmt = select(PointRule).where( # A GamificationService.award_points() kezeli a point_rules olvasást, szorzókat,
PointRule.action_key == action_key, # szintlépést, büntetés ledolgozást és a naplózást (PointsLedger).
PointRule.is_active == True,
)
rule_result = await db.execute(rule_stmt)
rule = rule_result.scalar_one_or_none()
if not rule:
logger.warning(
f"Pontszabály '{action_key}' nem található vagy inaktív. "
f"Pontjóváírás kihagyva user_id={user_id}."
)
return 0
points_to_award = rule.points
logger.info(
f"Dinamikus pont lekérés: action_key='{action_key}', "
f"points={points_to_award} (forrás: gamification.point_rules tábla)"
)
# 2. Pontok jóváírása a Gamification Service-en keresztül
# A GamificationService.award_points() kezeli a szorzókat, szintlépést,
# büntetés ledolgozást és a naplózást (PointsLedger).
await gamification_service.award_points( await gamification_service.award_points(
db=db, db=db,
user_id=user_id, user_id=user_id,
amount=points_to_award, amount=0,
reason=f"ADD_NEW_PROVIDER: +{points_to_award} pont új szolgáltató rögzítéséért", reason=f"{action_key}",
commit=False, # A hívó (quick_add_provider) kezeli a commit-ot commit=False, # A hívó kezeli a commit-ot
action_key=action_key,
) )
# 3. Statisztika növelése (providers_added_count) # 2. Statisztika növelése (providers_added_count)
stats_stmt = select(UserStats).where(UserStats.user_id == user_id) stats_stmt = select(UserStats).where(UserStats.user_id == user_id)
stats = (await db.execute(stats_stmt)).scalar_one_or_none() stats = (await db.execute(stats_stmt)).scalar_one_or_none()
@@ -122,7 +105,7 @@ async def _award_provider_points(
# Ha még nincs UserStats rekordja, létrehozzuk # Ha még nincs UserStats rekordja, létrehozzuk
stats = UserStats( stats = UserStats(
user_id=user_id, user_id=user_id,
total_xp=points_to_award, total_xp=0,
current_level=1, current_level=1,
providers_added_count=1, providers_added_count=1,
) )
@@ -132,7 +115,102 @@ async def _award_provider_points(
f"providers_added_count=1" f"providers_added_count=1"
) )
return points_to_award return 0 # Visszatérési érték már nem a pontszám, mert az award_points kezeli
async def find_or_create_provider_by_name(
db: AsyncSession,
name: str,
added_by_user_id: int,
) -> tuple:
"""
Keres vagy létrehoz egy ServiceProvider-t a megadott név alapján.
Ez a függvény az expense auto-discovery hook része. Amikor egy user
költséget rögzít és megad egy external_vendor_name-t, ez a függvény
megkeresi, hogy létezik-e már a provider, és ha nem, létrehozza.
Logika:
1. Pontos match keresése ServiceProvider.name alapján (kisbetűs, space-sztrippelt)
2. Ha nem létezik → új provider létrehozása PENDING státusszal,
validation_score=10, source=import, vissza: PROVIDER_DISCOVERY
3. Ha létezik és APPROVED → vissza: PROVIDER_VERIFIED_USE
4. Ha létezik és nem APPROVED → vissza: PROVIDER_CONFIRMATION
5. A függvény meghívja a _award_provider_points()-t a megfelelő action_key-kel
Args:
db: AsyncSession
name: A provider neve (external_vendor_name)
added_by_user_id: A költséget rögzítő user ID-ja
Returns:
tuple[ServiceProvider, str]: (provider, action_key)
- action_key: "PROVIDER_DISCOVERY" | "PROVIDER_CONFIRMATION" | "PROVIDER_VERIFIED_USE"
"""
# 1. Pontos match keresése (kisbetűsen, space-sztrippelten)
clean_name = name.strip()
provider_stmt = select(ServiceProvider).where(
func.lower(ServiceProvider.name) == func.lower(clean_name)
)
provider_result = await db.execute(provider_stmt)
provider = provider_result.scalar_one_or_none()
if not provider:
# 2. Ha nem létezik → létrehozás + PROVIDER_DISCOVERY
new_provider = ServiceProvider(
name=clean_name,
address=clean_name,
status=ModerationStatus.pending,
source=SourceType.api,
validation_score=10,
added_by_user_id=added_by_user_id,
)
db.add(new_provider)
await db.flush()
logger.info(
f"Új provider felfedezve: name='{clean_name}', "
f"provider_id={new_provider.id}, user_id={added_by_user_id}"
)
# Gamification pont kiosztása
await _award_provider_points(
db=db,
user_id=added_by_user_id,
action_key="PROVIDER_DISCOVERY",
)
return new_provider, "PROVIDER_DISCOVERY"
# 3. Ha létezik, státusz alapján döntés
# P0 CRITICAL (Card #362): USE_UNVERIFIED_PROVIDER vs PROVIDER_CONFIRMATION
# - Ha a provider APPROVED -> PROVIDER_VERIFIED_USE (100 XP)
# - Ha a provider PENDING es a hasznalo user a provider letrehozoja -> PROVIDER_CONFIRMATION (150 XP)
# - Ha a provider PENDING es a hasznalo user NEM a provider letrehozoja -> USE_UNVERIFIED_PROVIDER (200 XP)
# Ez megakadalyozza az XP farmingot (sajat provider hasznalata sajat maganak).
if provider.status == ModerationStatus.approved:
action_key = "PROVIDER_VERIFIED_USE"
elif provider.added_by_user_id == added_by_user_id:
# A provider letrehozoja hasznalja a sajat pending provideret
action_key = "PROVIDER_CONFIRMATION"
else:
# Mas user hasznal egy pending providert
action_key = "USE_UNVERIFIED_PROVIDER"
logger.info(
f"Meglevo provider hasznalata: name='{clean_name}', "
f"provider_id={provider.id}, action_key='{action_key}', "
f"user_id={added_by_user_id}, provider_creator_id={provider.added_by_user_id}"
)
# Gamification pont kiosztása
await _award_provider_points(
db=db,
user_id=added_by_user_id,
action_key=action_key,
)
return provider, action_key
async def search_providers( async def search_providers(

View File

@@ -10,36 +10,36 @@ from app.schemas.social import ServiceProviderCreate
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SocialService: class SocialService:
""" """
SocialService: Kezeli a közösségi interakciókat, szavazatokat és a moderációt. SocialService: Kezeli a közösségi interakciókat, szavazatokat és a moderációt.
Az importok a metódusokon belül vannak a körkörös függőség elkerülése érdekében. Az importok a metódusokon belül vannak a körkörös függőség elkerülése érdekében.
""" """
async def create_service_provider(self, db: AsyncSession, obj_in: ServiceProviderCreate, user_id: int): async def create_service_provider(self, db: AsyncSession, obj_in: ServiceProviderCreate, user_id: int):
from app.services.gamification_service import gamification_service from app.services.provider_service import _award_provider_points
new_provider = ServiceProvider(**obj_in.model_dump(), added_by_user_id=user_id) new_provider = ServiceProvider(**obj_in.model_dump(), added_by_user_id=user_id)
db.add(new_provider) db.add(new_provider)
await db.flush() await db.flush()
# Alappontszám az új beküldésért # Dinamikus pontjóváírás a point_rules táblából (ADD_NEW_PROVIDER)
await gamification_service.process_activity(db, user_id, 50, 10, f"New Provider: {new_provider.name}") await _award_provider_points(db, user_id, "ADD_NEW_PROVIDER")
await db.commit() await db.commit()
await db.refresh(new_provider) await db.refresh(new_provider)
return new_provider return new_provider
async def vote_for_provider(self, db: AsyncSession, voter_id: int, provider_id: int, vote_value: int): async def vote_for_provider(self, db: AsyncSession, voter_id: int, provider_id: int, vote_value: int):
from app.services.gamification_service import gamification_service from app.services.provider_service import _award_provider_points
# Duplikált szavazat ellenőrzése # Duplikált szavazat ellenőrzése
exists = (await db.execute(select(Vote).where(and_(Vote.user_id == voter_id, Vote.provider_id == provider_id)))).scalar() exists = (await db.execute(select(Vote).where(and_(Vote.user_id == voter_id, Vote.provider_id == provider_id)))).scalar()
if exists: if exists:
return {"message": "Már szavaztál erre a szolgáltatóra!"} return {"message": "Már szavaztál erre a szolgáltatóra!"}
db.add(Vote(user_id=voter_id, provider_id=provider_id, vote_value=vote_value)) db.add(Vote(user_id=voter_id, provider_id=provider_id, vote_value=vote_value))
provider = (await db.execute(select(ServiceProvider).where(ServiceProvider.id == provider_id))).scalar_one_or_none() provider = (await db.execute(select(ServiceProvider).where(ServiceProvider.id == provider_id))).scalar_one_or_none()
if not provider: if not provider:
return {"error": "Szolgáltató nem található."} return {"error": "Szolgáltató nem található."}
provider.validation_score += vote_value provider.validation_score += vote_value
@@ -53,6 +53,9 @@ class SocialService:
provider.status = ModerationStatus.rejected provider.status = ModerationStatus.rejected
await self._penalize_user(db, provider.added_by_user_id, provider.name) await self._penalize_user(db, provider.added_by_user_id, provider.name)
# RATE_PROVIDER pontok kiosztása a sikeres szavazatért
await _award_provider_points(db, voter_id, "RATE_PROVIDER")
await db.commit() await db.commit()
return {"status": "success", "score": provider.validation_score, "new_status": provider.status} return {"status": "success", "score": provider.validation_score, "new_status": provider.status}
@@ -67,7 +70,8 @@ class SocialService:
from app.services.gamification_service import gamification_service from app.services.gamification_service import gamification_service
if not user_id: return if not user_id: return
await gamification_service.process_activity(db, user_id, 100, 20, f"Validated: {provider_name}") # A pontérték a point_rules táblából jön (action_key='PROVIDER_VALIDATED')
await gamification_service.process_activity(db, user_id, xp_amount=0, social_amount=20, reason=f"Validated: {provider_name}", action_key="PROVIDER_VALIDATED")
# Aktuális verseny keresése és pontozása # Aktuális verseny keresése és pontozása
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
@@ -91,8 +95,8 @@ class SocialService:
from app.services.gamification_service import gamification_service from app.services.gamification_service import gamification_service
if not user_id: return if not user_id: return
# JAVÍTVA: is_penalty=True hozzáadva a gamification híváshoz # A pontérték a point_rules táblából jön (action_key='PROVIDER_REJECTED')
await gamification_service.process_activity(db, user_id, 50, 0, f"Rejected: {provider_name}", is_penalty=True) await gamification_service.process_activity(db, user_id, xp_amount=0, social_amount=0, reason=f"Rejected: {provider_name}", is_penalty=True, action_key="PROVIDER_REJECTED")
user = (await db.execute(select(User).where(User.id == user_id))).scalar_one_or_none() user = (await db.execute(select(User).where(User.id == user_id))).scalar_one_or_none()
if user and hasattr(user, 'reputation_score'): if user and hasattr(user, 'reputation_score'):

View File

@@ -0,0 +1,186 @@
"""
Test: USE_UNVERIFIED_PROVIDER pontszabály bekötése (Card #362)
=============================================================
Ellenőrzi, hogy a find_or_create_provider_by_name() függvény helyesen
differenciál a PROVIDER_CONFIRMATION és USE_UNVERIFIED_PROVIDER között.
Logika:
1. Ha a provider APPROVED -> PROVIDER_VERIFIED_USE (100 XP)
2. Ha a provider PENDING és a használó user a provider létrehozója -> PROVIDER_CONFIRMATION (150 XP)
3. Ha a provider PENDING és a használó user NEM a provider létrehozója -> USE_UNVERIFIED_PROVIDER (200 XP)
Használat:
docker compose exec sf_api python3 /app/backend/tests/active/test_use_unverified_provider.py
"""
import asyncio
import logging
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "backend"))
from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from app.core.config import settings
from app.models.identity.social import ServiceProvider, ModerationStatus, SourceType
from app.models.gamification.gamification import PointRule, UserStats, PointsLedger
from app.services.provider_service import find_or_create_provider_by_name, _award_provider_points
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("Test-USE_UNVERIFIED_PROVIDER")
TEST_USER_ID = 1 # Provider creator (superadmin@profibot.hu)
TEST_OTHER_USER_ID = 2 # Other user using the provider (admin@profibot.hu)
TEST_PROVIDER_NAME = "Test Unverified Provider Card362"
async def cleanup_test_data(db: AsyncSession):
"""Clean up any leftover test data."""
await db.execute(
delete(PointsLedger).where(
PointsLedger.user_id.in_([TEST_USER_ID, TEST_OTHER_USER_ID])
)
)
await db.execute(
delete(UserStats).where(
UserStats.user_id.in_([TEST_USER_ID, TEST_OTHER_USER_ID])
)
)
await db.execute(
delete(ServiceProvider).where(ServiceProvider.name == TEST_PROVIDER_NAME)
)
await db.commit()
async def test_use_unverified_provider():
"""
Test scenario:
1. User A (TEST_USER_ID) creates a provider via find_or_create_provider_by_name()
-> Should return PROVIDER_DISCOVERY (300 XP) - new provider
2. User A uses the same provider again
-> Should return PROVIDER_CONFIRMATION (150 XP) - same user, pending provider
3. User B (TEST_OTHER_USER_ID) uses the same provider
-> Should return USE_UNVERIFIED_PROVIDER (200 XP) - different user, pending provider
"""
engine = create_async_engine(settings.DATABASE_URL, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as db:
try:
# Clean up first
await cleanup_test_data(db)
# =========================================================
# STEP 1: User A creates a new provider (PROVIDER_DISCOVERY)
# =========================================================
logger.info("=" * 60)
logger.info("STEP 1: User A creates a new provider")
logger.info("=" * 60)
provider, action_key = await find_or_create_provider_by_name(
db=db,
name=TEST_PROVIDER_NAME,
added_by_user_id=TEST_USER_ID,
)
assert provider is not None, "Provider should be created"
assert action_key == "PROVIDER_DISCOVERY", (
f"Expected PROVIDER_DISCOVERY, got {action_key}"
)
assert provider.status == ModerationStatus.pending, (
f"Expected pending status, got {provider.status}"
)
assert provider.added_by_user_id == TEST_USER_ID, (
f"Expected added_by_user_id={TEST_USER_ID}, got {provider.added_by_user_id}"
)
logger.info(f"✅ STEP 1 PASS: action_key={action_key}, provider_id={provider.id}")
# Check that points were awarded
stats_stmt = select(UserStats).where(UserStats.user_id == TEST_USER_ID)
stats = (await db.execute(stats_stmt)).scalar_one_or_none()
assert stats is not None, "UserStats should exist"
assert stats.total_xp >= 300, (
f"Expected at least 300 XP, got {stats.total_xp}"
)
logger.info(f"✅ STEP 1 XP CHECK: total_xp={stats.total_xp}")
# =========================================================
# STEP 2: User A uses the same provider again (PROVIDER_CONFIRMATION)
# =========================================================
logger.info("=" * 60)
logger.info("STEP 2: User A uses the same provider again")
logger.info("=" * 60)
# Re-fetch to get clean state
await db.refresh(provider)
provider2, action_key2 = await find_or_create_provider_by_name(
db=db,
name=TEST_PROVIDER_NAME,
added_by_user_id=TEST_USER_ID,
)
assert provider2.id == provider.id, "Should be the same provider"
assert action_key2 == "PROVIDER_CONFIRMATION", (
f"Expected PROVIDER_CONFIRMATION, got {action_key2}"
)
logger.info(f"✅ STEP 2 PASS: action_key={action_key2}")
# =========================================================
# STEP 3: User B uses the same provider (USE_UNVERIFIED_PROVIDER)
# =========================================================
logger.info("=" * 60)
logger.info("STEP 3: User B uses the same provider")
logger.info("=" * 60)
provider3, action_key3 = await find_or_create_provider_by_name(
db=db,
name=TEST_PROVIDER_NAME,
added_by_user_id=TEST_OTHER_USER_ID,
)
assert provider3.id == provider.id, "Should be the same provider"
assert action_key3 == "USE_UNVERIFIED_PROVIDER", (
f"Expected USE_UNVERIFIED_PROVIDER, got {action_key3}"
)
logger.info(f"✅ STEP 3 PASS: action_key={action_key3}")
# Check that User B got points
stats_stmt2 = select(UserStats).where(UserStats.user_id == TEST_OTHER_USER_ID)
stats2 = (await db.execute(stats_stmt2)).scalar_one_or_none()
assert stats2 is not None, "UserStats should exist for User B"
assert stats2.total_xp >= 200, (
f"Expected at least 200 XP for User B, got {stats2.total_xp}"
)
logger.info(f"✅ STEP 3 XP CHECK: User B total_xp={stats2.total_xp}")
# =========================================================
# SUMMARY
# =========================================================
logger.info("=" * 60)
logger.info("🎉 ALL TESTS PASSED!")
logger.info(f" - PROVIDER_DISCOVERY (300 XP): ✅ User A created provider")
logger.info(f" - PROVIDER_CONFIRMATION (150 XP): ✅ User A reused own provider")
logger.info(f" - USE_UNVERIFIED_PROVIDER (200 XP): ✅ User B used pending provider")
logger.info("=" * 60)
# Clean up
await cleanup_test_data(db)
except AssertionError as e:
logger.error(f"❌ TEST FAILED: {e}")
await db.rollback()
await cleanup_test_data(db)
sys.exit(1)
except Exception as e:
logger.error(f"❌ UNEXPECTED ERROR: {e}", exc_info=True)
await db.rollback()
await cleanup_test_data(db)
sys.exit(1)
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(test_use_unverified_provider())

View File

@@ -0,0 +1,28 @@
"""feat: add vehicle classes and specializations to providers
Revision ID: 2387cd18fde7
Revises: 7cd9b8a65ce8
Create Date: 2026-07-01 00:38:21.828081
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '2387cd18fde7'
down_revision: Union[str, Sequence[str], None] = '7cd9b8a65ce8'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@@ -0,0 +1,131 @@
{
"AUTH": {
"LOGIN": {
"SUCCESS": "Přihlášení úspěšné. Vítejte zpět!"
},
"ERROR": {
"EMAIL_EXISTS": "Tento email je již zaregistrován.",
"UNAUTHORIZED": "Neoprávněný přístup."
}
},
"SENTINEL": {
"LOCK": {
"MSG": "Účet byl z bezpečnostních důvodů uzamčen."
},
"APPROVAL": {
"REQUIRED": "Vyžadováno schválení"
}
},
"COMMON": {
"SAVE_SUCCESS": "Úspěšně uloženo!"
},
"EMAIL": {
"REG": {
"SUBJECT": "Potvrďte svou registraci - Service Finder",
"GREETING": "Ahoj {{first_name}}!",
"BODY": "Děkujeme za registraci! Kliknutím na tlačítko níže aktivujete svůj účet:",
"BUTTON": "Aktivovat účet",
"FOOTER": "Pokud jste se nezaregistrovali, ignorujte prosím tento email."
},
"PWD_RESET": {
"SUBJECT": "Žádost o obnovení hesla",
"GREETING": "Ahoj!",
"BODY": "Požádali jste o obnovení hesla. Kliknutím na tlačítko níže nastavíte nové heslo:",
"BUTTON": "Obnovit heslo",
"FOOTER": "Pokud jste o obnovení hesla nežádali, ignorujte prosím tento email."
},
"TEST": {
"SUBJECT": "🧪 Testovací email - Service Finder",
"GREETING": "Ahoj {{first_name}}!",
"BODY": "Toto je testovací email ze systému Service Finder. Pokud to vidíte, odesílání emailů funguje!",
"BUTTON": "Přejít na Dashboard",
"FOOTER": "Service Finder - Testovací systémová zpráva"
}
},
"ORGANIZATION": {
"ERROR": {
"NOT_FOUND": "Organizace nenalezena.",
"INVALID_TAX_FORMAT": "Neplatný formát maďarského daňového čísla!",
"TAX_LOOKUP_FAILED": "Ověření daňového čísla selhalo.",
"FORBIDDEN": "Nemáte oprávnění k přístupu k této organizaci.",
"INVALID_ROLE": "Neplatná role.",
"CANNOT_REMOVE_OWNER": "Vlastník nemůže být odstraněn z organizace.",
"CANNOT_DEMOTE_OWNER": "Role vlastníka nemůže být změněna.",
"ALREADY_MEMBER": "Již jste členem této organizace.",
"NO_ACTIVE_ADMIN": "Tato organizace nemá aktivního správce. Použijte koncový bod /claim.",
"HAS_ACTIVE_ADMIN": "Tato organizace má aktivního správce. Použijte koncový bod /join-request.",
"EMAIL_MISMATCH": "Email neodpovídá emailu vlastníka organizace.",
"INVALID_CODE": "Neplatný nebo expirovaný kód.",
"CODE_ORG_MISMATCH": "Kód nepatří k této organizaci.",
"INVITE_EXPIRED": "Pozvánka vypršela.",
"INVITE_INVALID": "Neplatný token pozvánky."
},
"SUCCESS": {
"ONBOARDED": "Společnost úspěšně vytvořena a zaregistrována.",
"MEMBER_REMOVED": "Člen úspěšně odstraněn.",
"ROLE_UPDATED": "Role úspěšně aktualizována.",
"UPDATED": "Data organizace úspěšně aktualizována.",
"VISUAL_SETTINGS_UPDATED": "Vizuální nastavení úspěšně aktualizováno."
},
"INVITATION": {
"CREATED": "Pozvánka úspěšně vytvořena.",
"ACCEPTED": "Pozvánka přijata.",
"REVOKED": "Pozvánka odvolána."
},
"JOIN_REQUEST": {
"SENT": "Vaše žádost o připojení byla odeslána. Čeká na schválení správcem."
},
"CLAIM": {
"OTP_SENT": "6místný ověřovací kód byl odeslán na email vlastníka organizace.",
"SUCCESS": "Úspěšně jste převzali organizaci. Nyní jste správcem."
}
},
"REGISTRATION_DOCUMENTS": {
"CERTIFICATE_NUMBER": "Číslo osvědčení o registraci",
"CERTIFICATE_VALIDITY": "Platnost osvědčení o registraci",
"DOCUMENT_NUMBER": "Číslo dokladu vozidla",
"TITLE": "Osvědčení o registraci & Doklad vozidla"
},
"GAMIFICATION": {
"SUBMIT_SERVICE": {
"SELF_DEFENSE_BLOCKED": "Z důvodu omezení sebeobrany nemůžete odesílat nová servisní data.",
"SUCCESS": "Servis odeslán do systému k analýze!",
"POINTS_ERROR": "Chyba při bodování: {error}",
"SUBMIT_ERROR": "Chyba při odesílání: {error}"
},
"QUIZ": {
"ALREADY_PLAYED": "Dnes jste již hráli denní kvíz. Vraťte se zítra!",
"NO_QUIZ_AVAILABLE": "Dnes není k dispozici žádný kvíz.",
"ALREADY_COMPLETED": "Denní kvíz byl dnes již označen jako dokončený.",
"COMPLETED_SUCCESS": "Denní kvíz označen jako dokončený pro dnešek.",
"QUESTION_NOT_FOUND": "Otázka nenalezena.",
"POINTS_FAILED": "Nepodařilo se udělit body za kvíz: {error}"
},
"BADGE": {
"NOT_FOUND": "Odznak nenalezen.",
"ALREADY_AWARDED": "Odznak již byl tomuto uživateli udělen.",
"AWARDED_SUCCESS": "Odznak '{badge_name}' byl udělen uživateli.",
"POINTS_FAILED": "Nepodařilo se udělit body za odznak: {error}"
},
"SEASON": {
"NOT_FOUND": "Sezóna nenalezena.",
"NO_ACTIVE": "Nebyla nalezena žádná aktivní sezóna."
},
"ACHIEVEMENTS": {
"XP_NOVICE": "Začátečník",
"XP_APPRENTICE": "Učeň",
"XP_EXPERT": "Expert",
"XP_MASTER": "Mistr",
"XP_NOVICE_DESC": "Získej 100 XP",
"XP_APPRENTICE_DESC": "Získej 500 XP",
"XP_EXPERT_DESC": "Získej 2000 XP",
"XP_MASTER_DESC": "Získej 5000 XP",
"QUIZ_BEGINNER": "Kvízový začátečník",
"QUIZ_ENTHUSIAST": "Kvízový nadšenec",
"QUIZ_MASTER": "Kvízový mistr",
"QUIZ_BEGINNER_DESC": "Získej 50 kvízových bodů",
"QUIZ_ENTHUSIAST_DESC": "Získej 200 kvízových bodů",
"QUIZ_MASTER_DESC": "Získej 500 kvízových bodů"
}
}
}

View File

@@ -0,0 +1,131 @@
{
"AUTH": {
"LOGIN": {
"SUCCESS": "Anmeldung erfolgreich. Willkommen zurück!"
},
"ERROR": {
"EMAIL_EXISTS": "Diese E-Mail ist bereits registriert.",
"UNAUTHORIZED": "Unbefugter Zugriff."
}
},
"SENTINEL": {
"LOCK": {
"MSG": "Konto aus Sicherheitsgründen gesperrt."
},
"APPROVAL": {
"REQUIRED": "Genehmigung erforderlich"
}
},
"COMMON": {
"SAVE_SUCCESS": "Erfolgreich gespeichert!"
},
"EMAIL": {
"REG": {
"SUBJECT": "Bestätigen Sie Ihre Registrierung - Service Finder",
"GREETING": "Hallo {{first_name}}!",
"BODY": "Vielen Dank für Ihre Registrierung! Klicken Sie auf die Schaltfläche unten, um Ihr Konto zu aktivieren:",
"BUTTON": "Konto aktivieren",
"FOOTER": "Wenn Sie sich nicht registriert haben, ignorieren Sie bitte diese E-Mail."
},
"PWD_RESET": {
"SUBJECT": "Passwort zurücksetzen",
"GREETING": "Hallo!",
"BODY": "Sie haben eine Passwortzurücksetzung angefordert. Klicken Sie auf die Schaltfläche unten, um ein neues Passwort festzulegen:",
"BUTTON": "Passwort zurücksetzen",
"FOOTER": "Wenn Sie keine Passwortzurücksetzung angefordert haben, ignorieren Sie bitte diese E-Mail."
},
"TEST": {
"SUBJECT": "🧪 Test-E-Mail - Service Finder",
"GREETING": "Hallo {{first_name}}!",
"BODY": "Dies ist eine Test-E-Mail vom Service Finder System. Wenn Sie dies sehen, funktioniert der E-Mail-Versand!",
"BUTTON": "Zum Dashboard",
"FOOTER": "Service Finder - Test-Systemnachricht"
}
},
"ORGANIZATION": {
"ERROR": {
"NOT_FOUND": "Organisation nicht gefunden.",
"INVALID_TAX_FORMAT": "Ungültiges ungarisches Steuernummernformat!",
"TAX_LOOKUP_FAILED": "Steuernummernprüfung fehlgeschlagen.",
"FORBIDDEN": "Sie haben keine Berechtigung für diese Organisation.",
"INVALID_ROLE": "Ungültige Rolle angegeben.",
"CANNOT_REMOVE_OWNER": "Der Eigentümer kann nicht aus der Organisation entfernt werden.",
"CANNOT_DEMOTE_OWNER": "Die Rolle des Eigentümers kann nicht geändert werden.",
"ALREADY_MEMBER": "Sie sind bereits Mitglied dieser Organisation.",
"NO_ACTIVE_ADMIN": "Diese Organisation hat keinen aktiven Administrator. Verwenden Sie den /claim-Endpunkt.",
"HAS_ACTIVE_ADMIN": "Diese Organisation hat einen aktiven Administrator. Verwenden Sie den /join-request-Endpunkt.",
"EMAIL_MISMATCH": "Die E-Mail stimmt nicht mit der E-Mail des Organisationsinhabers überein.",
"INVALID_CODE": "Ungültiger oder abgelaufener Code.",
"CODE_ORG_MISMATCH": "Der Code gehört nicht zu dieser Organisation.",
"INVITE_EXPIRED": "Die Einladung ist abgelaufen.",
"INVITE_INVALID": "Ungültiges Einladungstoken."
},
"SUCCESS": {
"ONBOARDED": "Unternehmen erfolgreich erstellt und registriert.",
"MEMBER_REMOVED": "Mitglied erfolgreich entfernt.",
"ROLE_UPDATED": "Rolle erfolgreich aktualisiert.",
"UPDATED": "Organisationsdaten erfolgreich aktualisiert.",
"VISUAL_SETTINGS_UPDATED": "Visuelle Einstellungen erfolgreich aktualisiert."
},
"INVITATION": {
"CREATED": "Einladung erfolgreich erstellt.",
"ACCEPTED": "Einladung angenommen.",
"REVOKED": "Einladung widerrufen."
},
"JOIN_REQUEST": {
"SENT": "Ihre Beitrittsanfrage wurde übermittelt. Warten auf Administratorgenehmigung."
},
"CLAIM": {
"OTP_SENT": "Ein 6-stelliger Bestätigungscode wurde an die E-Mail des Organisationsinhabers gesendet.",
"SUCCESS": "Sie haben die Organisation erfolgreich übernommen. Sie sind jetzt der Administrator."
}
},
"REGISTRATION_DOCUMENTS": {
"CERTIFICATE_NUMBER": "Zulassungsbescheinigung Nummer",
"CERTIFICATE_VALIDITY": "Gültigkeit der Zulassungsbescheinigung",
"DOCUMENT_NUMBER": "Fahrzeugdokument Nummer",
"TITLE": "Zulassungsbescheinigung & Fahrzeugdokument"
},
"GAMIFICATION": {
"SUBMIT_SERVICE": {
"SELF_DEFENSE_BLOCKED": "Sie können aufgrund einer Selbstverteidigungseinschränkung keine neuen Servicedaten einreichen.",
"SUCCESS": "Service zur Analyse an das System übermittelt!",
"POINTS_ERROR": "Fehler bei der Bewertung: {error}",
"SUBMIT_ERROR": "Fehler bei der Übermittlung: {error}"
},
"QUIZ": {
"ALREADY_PLAYED": "Sie haben heute bereits am täglichen Quiz teilgenommen. Kommen Sie morgen wieder!",
"NO_QUIZ_AVAILABLE": "Heute ist kein Quiz verfügbar.",
"ALREADY_COMPLETED": "Das tägliche Quiz wurde heute bereits als abgeschlossen markiert.",
"COMPLETED_SUCCESS": "Tägliches Quiz wurde heute als abgeschlossen markiert.",
"QUESTION_NOT_FOUND": "Frage nicht gefunden.",
"POINTS_FAILED": "Fehler bei der Vergabe von Quizpunkten: {error}"
},
"BADGE": {
"NOT_FOUND": "Abzeichen nicht gefunden.",
"ALREADY_AWARDED": "Abzeichen wurde diesem Benutzer bereits verliehen.",
"AWARDED_SUCCESS": "Abzeichen '{badge_name}' wurde dem Benutzer verliehen.",
"POINTS_FAILED": "Fehler bei der Vergabe von Abzeichenpunkten: {error}"
},
"SEASON": {
"NOT_FOUND": "Saison nicht gefunden.",
"NO_ACTIVE": "Keine aktive Saison gefunden."
},
"ACHIEVEMENTS": {
"XP_NOVICE": "Anfänger",
"XP_APPRENTICE": "Lehrling",
"XP_EXPERT": "Experte",
"XP_MASTER": "Meister",
"XP_NOVICE_DESC": "Sammle 100 XP",
"XP_APPRENTICE_DESC": "Sammle 500 XP",
"XP_EXPERT_DESC": "Sammle 2000 XP",
"XP_MASTER_DESC": "Sammle 5000 XP",
"QUIZ_BEGINNER": "Quiz-Anfänger",
"QUIZ_ENTHUSIAST": "Quiz-Enthusiast",
"QUIZ_MASTER": "Quiz-Meister",
"QUIZ_BEGINNER_DESC": "Sammle 50 Quizpunkte",
"QUIZ_ENTHUSIAST_DESC": "Sammle 200 Quizpunkte",
"QUIZ_MASTER_DESC": "Sammle 500 Quizpunkte"
}
}
}

View File

@@ -0,0 +1,131 @@
{
"AUTH": {
"LOGIN": {
"SUCCESS": "Connexion réussie. Bon retour parmi nous!"
},
"ERROR": {
"EMAIL_EXISTS": "Cet email est déjà enregistré.",
"UNAUTHORIZED": "Accès non autorisé."
}
},
"SENTINEL": {
"LOCK": {
"MSG": "Compte verrouillé pour des raisons de sécurité."
},
"APPROVAL": {
"REQUIRED": "Approbation requise"
}
},
"COMMON": {
"SAVE_SUCCESS": "Enregistré avec succès!"
},
"EMAIL": {
"REG": {
"SUBJECT": "Confirmez votre inscription - Service Finder",
"GREETING": "Bonjour {{first_name}}!",
"BODY": "Merci de vous être inscrit! Cliquez sur le bouton ci-dessous pour activer votre compte:",
"BUTTON": "Activer le compte",
"FOOTER": "Si vous ne vous êtes pas inscrit, veuillez ignorer cet email."
},
"PWD_RESET": {
"SUBJECT": "Demande de réinitialisation du mot de passe",
"GREETING": "Bonjour!",
"BODY": "Vous avez demandé une réinitialisation de mot de passe. Cliquez sur le bouton ci-dessous pour définir un nouveau mot de passe:",
"BUTTON": "Réinitialiser le mot de passe",
"FOOTER": "Si vous n'avez pas demandé de réinitialisation, veuillez ignorer cet email."
},
"TEST": {
"SUBJECT": "🧪 Email de test - Service Finder",
"GREETING": "Bonjour {{first_name}}!",
"BODY": "Ceci est un email de test du système Service Finder. Si vous voyez ceci, l'envoi d'emails fonctionne!",
"BUTTON": "Aller au tableau de bord",
"FOOTER": "Service Finder - Message système de test"
}
},
"ORGANIZATION": {
"ERROR": {
"NOT_FOUND": "Organisation non trouvée.",
"INVALID_TAX_FORMAT": "Format de numéro de TVA hongrois invalide!",
"TAX_LOOKUP_FAILED": "La vérification du numéro de TVA a échoué.",
"FORBIDDEN": "Vous n'avez pas la permission d'accéder à cette organisation.",
"INVALID_ROLE": "Rôle spécifié invalide.",
"CANNOT_REMOVE_OWNER": "Le propriétaire ne peut pas être retiré de l'organisation.",
"CANNOT_DEMOTE_OWNER": "Le rôle du propriétaire ne peut pas être modifié.",
"ALREADY_MEMBER": "Vous êtes déjà membre de cette organisation.",
"NO_ACTIVE_ADMIN": "Cette organisation n'a pas d'administrateur actif. Utilisez le point de terminaison /claim.",
"HAS_ACTIVE_ADMIN": "Cette organisation a un administrateur actif. Utilisez le point de terminaison /join-request.",
"EMAIL_MISMATCH": "L'email ne correspond pas à l'email du propriétaire de l'organisation.",
"INVALID_CODE": "Code invalide ou expiré.",
"CODE_ORG_MISMATCH": "Le code n'appartient pas à cette organisation.",
"INVITE_EXPIRED": "L'invitation a expiré.",
"INVITE_INVALID": "Jeton d'invitation invalide."
},
"SUCCESS": {
"ONBOARDED": "Entreprise créée et enregistrée avec succès.",
"MEMBER_REMOVED": "Membre supprimé avec succès.",
"ROLE_UPDATED": "Rôle mis à jour avec succès.",
"UPDATED": "Données de l'organisation mises à jour avec succès.",
"VISUAL_SETTINGS_UPDATED": "Paramètres visuels mis à jour avec succès."
},
"INVITATION": {
"CREATED": "Invitation créée avec succès.",
"ACCEPTED": "Invitation acceptée.",
"REVOKED": "Invitation révoquée."
},
"JOIN_REQUEST": {
"SENT": "Votre demande d'adhésion a été soumise. En attente de l'approbation de l'administrateur."
},
"CLAIM": {
"OTP_SENT": "Un code de vérification à 6 chiffres a été envoyé à l'email du propriétaire de l'organisation.",
"SUCCESS": "Vous avez repris l'organisation avec succès. Vous êtes maintenant l'administrateur."
}
},
"REGISTRATION_DOCUMENTS": {
"CERTIFICATE_NUMBER": "Numéro du certificat d'immatriculation",
"CERTIFICATE_VALIDITY": "Validité du certificat d'immatriculation",
"DOCUMENT_NUMBER": "Numéro du document du véhicule",
"TITLE": "Certificat d'immatriculation & Document du véhicule"
},
"GAMIFICATION": {
"SUBMIT_SERVICE": {
"SELF_DEFENSE_BLOCKED": "Vous ne pouvez pas soumettre de nouvelles données de service en raison d'une restriction d'autodéfense.",
"SUCCESS": "Service soumis au système pour analyse!",
"POINTS_ERROR": "Erreur lors de l'attribution des points: {error}",
"SUBMIT_ERROR": "Erreur lors de la soumission: {error}"
},
"QUIZ": {
"ALREADY_PLAYED": "Vous avez déjà participé au quiz quotidien aujourd'hui. Revenez demain!",
"NO_QUIZ_AVAILABLE": "Aucun quiz disponible aujourd'hui.",
"ALREADY_COMPLETED": "Le quiz quotidien a déjà été marqué comme terminé aujourd'hui.",
"COMPLETED_SUCCESS": "Quiz quotidien marqué comme terminé pour aujourd'hui.",
"QUESTION_NOT_FOUND": "Question non trouvée.",
"POINTS_FAILED": "Échec de l'attribution des points de quiz: {error}"
},
"BADGE": {
"NOT_FOUND": "Badge non trouvé.",
"ALREADY_AWARDED": "Badge déjà attribué à cet utilisateur.",
"AWARDED_SUCCESS": "Badge '{badge_name}' attribué à l'utilisateur.",
"POINTS_FAILED": "Échec de l'attribution des points de badge: {error}"
},
"SEASON": {
"NOT_FOUND": "Saison non trouvée.",
"NO_ACTIVE": "Aucune saison active trouvée."
},
"ACHIEVEMENTS": {
"XP_NOVICE": "Novice",
"XP_APPRENTICE": "Apprenti",
"XP_EXPERT": "Expert",
"XP_MASTER": "Maître",
"XP_NOVICE_DESC": "Gagnez 100 XP",
"XP_APPRENTICE_DESC": "Gagnez 500 XP",
"XP_EXPERT_DESC": "Gagnez 2000 XP",
"XP_MASTER_DESC": "Gagnez 5000 XP",
"QUIZ_BEGINNER": "Débutant Quiz",
"QUIZ_ENTHUSIAST": "Passionné de Quiz",
"QUIZ_MASTER": "Maître du Quiz",
"QUIZ_BEGINNER_DESC": "Gagnez 50 points de quiz",
"QUIZ_ENTHUSIAST_DESC": "Gagnez 200 points de quiz",
"QUIZ_MASTER_DESC": "Gagnez 500 points de quiz"
}
}
}

View File

@@ -0,0 +1,131 @@
{
"AUTH": {
"LOGIN": {
"SUCCESS": "Autentificare reușită. Bun venit înapoi!"
},
"ERROR": {
"EMAIL_EXISTS": "Acest email este deja înregistrat.",
"UNAUTHORIZED": "Acces neautorizat."
}
},
"SENTINEL": {
"LOCK": {
"MSG": "Cont blocat din motive de securitate."
},
"APPROVAL": {
"REQUIRED": "Aprobare necesară"
}
},
"COMMON": {
"SAVE_SUCCESS": "Salvat cu succes!"
},
"EMAIL": {
"REG": {
"SUBJECT": "Confirmați înregistrarea - Service Finder",
"GREETING": "Salut {{first_name}}!",
"BODY": "Vă mulțumim pentru înregistrare! Faceți clic pe butonul de mai jos pentru a vă activa contul:",
"BUTTON": "Activează Contul",
"FOOTER": "Dacă nu v-ați înregistrat, vă rugăm să ignorați acest email."
},
"PWD_RESET": {
"SUBJECT": "Solicitare resetare parolă",
"GREETING": "Salut!",
"BODY": "Ați solicitat resetarea parolei. Faceți clic pe butonul de mai jos pentru a seta o nouă parolă:",
"BUTTON": "Resetează Parola",
"FOOTER": "Dacă nu ați solicitat resetarea parolei, vă rugăm să ignorați acest email."
},
"TEST": {
"SUBJECT": "🧪 Email de test - Service Finder",
"GREETING": "Salut {{first_name}}!",
"BODY": "Acesta este un email de test de la sistemul Service Finder. Dacă vedeți acest mesaj, trimiterea de emailuri funcționează!",
"BUTTON": "Mergi la Dashboard",
"FOOTER": "Service Finder - Mesaj de sistem de test"
}
},
"ORGANIZATION": {
"ERROR": {
"NOT_FOUND": "Organizație negăsită.",
"INVALID_TAX_FORMAT": "Format invalid al numărului de TVA maghiar!",
"TAX_LOOKUP_FAILED": "Verificarea numărului de TVA a eșuat.",
"FORBIDDEN": "Nu aveți permisiunea de a accesa această organizație.",
"INVALID_ROLE": "Rol specificat invalid.",
"CANNOT_REMOVE_OWNER": "Proprietarul nu poate fi eliminat din organizație.",
"CANNOT_DEMOTE_OWNER": "Rolul proprietarului nu poate fi modificat.",
"ALREADY_MEMBER": "Sunteți deja membru al acestei organizații.",
"NO_ACTIVE_ADMIN": "Această organizație nu are un administrator activ. Utilizați punctul final /claim.",
"HAS_ACTIVE_ADMIN": "Această organizație are un administrator activ. Utilizați punctul final /join-request.",
"EMAIL_MISMATCH": "Emailul nu corespunde cu emailul proprietarului organizației.",
"INVALID_CODE": "Cod invalid sau expirat.",
"CODE_ORG_MISMATCH": "Codul nu aparține acestei organizații.",
"INVITE_EXPIRED": "Invitația a expirat.",
"INVITE_INVALID": "Token de invitație invalid."
},
"SUCCESS": {
"ONBOARDED": "Companie creată și înregistrată cu succes.",
"MEMBER_REMOVED": "Membru eliminat cu succes.",
"ROLE_UPDATED": "Rol actualizat cu succes.",
"UPDATED": "Datele organizației actualizate cu succes.",
"VISUAL_SETTINGS_UPDATED": "Setările vizuale actualizate cu succes."
},
"INVITATION": {
"CREATED": "Invitație creată cu succes.",
"ACCEPTED": "Invitație acceptată.",
"REVOKED": "Invitație revocată."
},
"JOIN_REQUEST": {
"SENT": "Cererea dvs. de aderare a fost trimisă. Se așteaptă aprobarea administratorului."
},
"CLAIM": {
"OTP_SENT": "Un cod de verificare din 6 cifre a fost trimis pe emailul proprietarului organizației.",
"SUCCESS": "Ați preluat cu succes organizația. Acum sunteți administratorul."
}
},
"REGISTRATION_DOCUMENTS": {
"CERTIFICATE_NUMBER": "Numărul certificatului de înmatriculare",
"CERTIFICATE_VALIDITY": "Valabilitatea certificatului de înmatriculare",
"DOCUMENT_NUMBER": "Numărul documentului vehiculului",
"TITLE": "Certificat de înmatriculare & Document vehicul"
},
"GAMIFICATION": {
"SUBMIT_SERVICE": {
"SELF_DEFENSE_BLOCKED": "Nu puteți trimite date noi de service din cauza unei restricții de autoapărare.",
"SUCCESS": "Service trimis sistemului pentru analiză!",
"POINTS_ERROR": "Eroare la punctare: {error}",
"SUBMIT_ERROR": "Eroare la trimitere: {error}"
},
"QUIZ": {
"ALREADY_PLAYED": "Ați jucat deja quizul zilnic astăzi. Reveniți mâine!",
"NO_QUIZ_AVAILABLE": "Niciun quiz disponibil pentru astăzi.",
"ALREADY_COMPLETED": "Quizul zilnic a fost deja marcat ca finalizat astăzi.",
"COMPLETED_SUCCESS": "Quizul zilnic marcat ca finalizat pentru astăzi.",
"QUESTION_NOT_FOUND": "Întrebare negăsită.",
"POINTS_FAILED": "Eroare la acordarea punctelor de quiz: {error}"
},
"BADGE": {
"NOT_FOUND": "Insignă negăsită.",
"ALREADY_AWARDED": "Insigna a fost deja acordată acestui utilizator.",
"AWARDED_SUCCESS": "Insigna '{badge_name}' a fost acordată utilizatorului.",
"POINTS_FAILED": "Eroare la acordarea punctelor insignei: {error}"
},
"SEASON": {
"NOT_FOUND": "Sezon negăsit.",
"NO_ACTIVE": "Niciun sezon activ găsit."
},
"ACHIEVEMENTS": {
"XP_NOVICE": "Începător",
"XP_APPRENTICE": "Ucenic",
"XP_EXPERT": "Expert",
"XP_MASTER": "Maestru",
"XP_NOVICE_DESC": "Câștigă 100 XP",
"XP_APPRENTICE_DESC": "Câștigă 500 XP",
"XP_EXPERT_DESC": "Câștigă 2000 XP",
"XP_MASTER_DESC": "Câștigă 5000 XP",
"QUIZ_BEGINNER": "Începător Quiz",
"QUIZ_ENTHUSIAST": "Entuziast Quiz",
"QUIZ_MASTER": "Maestru Quiz",
"QUIZ_BEGINNER_DESC": "Câștigă 50 de puncte de quiz",
"QUIZ_ENTHUSIAST_DESC": "Câștigă 200 de puncte de quiz",
"QUIZ_MASTER_DESC": "Câștigă 500 de puncte de quiz"
}
}
}

View File

@@ -0,0 +1,131 @@
{
"AUTH": {
"LOGIN": {
"SUCCESS": "Prihlásenie úspešné. Vitajte späť!"
},
"ERROR": {
"EMAIL_EXISTS": "Tento email je už zaregistrovaný.",
"UNAUTHORIZED": "Neoprávnený prístup."
}
},
"SENTINEL": {
"LOCK": {
"MSG": "Účet bol z bezpečnostných dôvodov uzamknutý."
},
"APPROVAL": {
"REQUIRED": "Vyžaduje sa schválenie"
}
},
"COMMON": {
"SAVE_SUCCESS": "Úspešne uložené!"
},
"EMAIL": {
"REG": {
"SUBJECT": "Potvrďte svoju registráciu - Service Finder",
"GREETING": "Ahoj {{first_name}}!",
"BODY": "Ďakujeme za registráciu! Kliknutím na tlačidlo nižšie aktivujete svoj účet:",
"BUTTON": "Aktivovať účet",
"FOOTER": "Ak ste sa nezaregistrovali, ignorujte prosím tento email."
},
"PWD_RESET": {
"SUBJECT": "Žiadosť o obnovenie hesla",
"GREETING": "Ahoj!",
"BODY": "Požiadali ste o obnovenie hesla. Kliknutím na tlačidlo nižšie nastavíte nové heslo:",
"BUTTON": "Obnoviť heslo",
"FOOTER": "Ak ste nepožiadali o obnovenie hesla, ignorujte prosím tento email."
},
"TEST": {
"SUBJECT": "🧪 Testovací email - Service Finder",
"GREETING": "Ahoj {{first_name}}!",
"BODY": "Toto je testovací email zo systému Service Finder. Ak to vidíte, odosielanie emailov funguje!",
"BUTTON": "Prejsť na Dashboard",
"FOOTER": "Service Finder - Testovacia systémová správa"
}
},
"ORGANIZATION": {
"ERROR": {
"NOT_FOUND": "Organizácia nenájdená.",
"INVALID_TAX_FORMAT": "Neplatný formát maďarského daňového čísla!",
"TAX_LOOKUP_FAILED": "Overenie daňového čísla zlyhalo.",
"FORBIDDEN": "Nemáte oprávnenie na prístup k tejto organizácii.",
"INVALID_ROLE": "Neplatná rola.",
"CANNOT_REMOVE_OWNER": "Vlastník nemôže byť odstránený z organizácie.",
"CANNOT_DEMOTE_OWNER": "Rola vlastníka nemôže byť zmenená.",
"ALREADY_MEMBER": "Už ste členom tejto organizácie.",
"NO_ACTIVE_ADMIN": "Táto organizácia nemá aktívneho správcu. Použite koncový bod /claim.",
"HAS_ACTIVE_ADMIN": "Táto organizácia má aktívneho správcu. Použite koncový bod /join-request.",
"EMAIL_MISMATCH": "Email nezodpovedá emailu vlastníka organizácie.",
"INVALID_CODE": "Neplatný alebo expirovaný kód.",
"CODE_ORG_MISMATCH": "Kód nepatrí k tejto organizácii.",
"INVITE_EXPIRED": "Pozvánka vypršala.",
"INVITE_INVALID": "Neplatný token pozvánky."
},
"SUCCESS": {
"ONBOARDED": "Spoločnosť úspešne vytvorená a zaregistrovaná.",
"MEMBER_REMOVED": "Člen úspešne odstránený.",
"ROLE_UPDATED": "Rola úspešne aktualizovaná.",
"UPDATED": "Dáta organizácie úspešne aktualizované.",
"VISUAL_SETTINGS_UPDATED": "Vizuálne nastavenia úspešne aktualizované."
},
"INVITATION": {
"CREATED": "Pozvánka úspešne vytvorená.",
"ACCEPTED": "Pozvánka prijatá.",
"REVOKED": "Pozvánka odvolaná."
},
"JOIN_REQUEST": {
"SENT": "Vaša žiadosť o pripojenie bola odoslaná. Čaká na schválenie správcom."
},
"CLAIM": {
"OTP_SENT": "6-miestny overovací kód bol odoslaný na email vlastníka organizácie.",
"SUCCESS": "Úspešne ste prevzali organizáciu. Teraz ste správcom."
}
},
"REGISTRATION_DOCUMENTS": {
"CERTIFICATE_NUMBER": "Číslo osvedčenia o registrácii",
"CERTIFICATE_VALIDITY": "Platnosť osvedčenia o registrácii",
"DOCUMENT_NUMBER": "Číslo dokladu vozidla",
"TITLE": "Osvedčenie o registrácii & Doklad vozidla"
},
"GAMIFICATION": {
"SUBMIT_SERVICE": {
"SELF_DEFENSE_BLOCKED": "Z dôvodu obmedzenia sebaobrany nemôžete odosielať nové servisné dáta.",
"SUCCESS": "Servis odoslaný do systému na analýzu!",
"POINTS_ERROR": "Chyba pri bodovaní: {error}",
"SUBMIT_ERROR": "Chyba pri odosielaní: {error}"
},
"QUIZ": {
"ALREADY_PLAYED": "Dnes ste už hrali denný kvíz. Vráťte sa zajtra!",
"NO_QUIZ_AVAILABLE": "Dnes nie je k dispozícii žiadny kvíz.",
"ALREADY_COMPLETED": "Denný kvíz bol dnes už označený ako dokončený.",
"COMPLETED_SUCCESS": "Denný kvíz označený ako dokončený pre dnešok.",
"QUESTION_NOT_FOUND": "Otázka nenájdená.",
"POINTS_FAILED": "Nepodarilo sa udeliť body za kvíz: {error}"
},
"BADGE": {
"NOT_FOUND": "Odznak nenájdený.",
"ALREADY_AWARDED": "Odznak už bol tomuto používateľovi udelený.",
"AWARDED_SUCCESS": "Odznak '{badge_name}' bol udelený používateľovi.",
"POINTS_FAILED": "Nepodarilo sa udeliť body za odznak: {error}"
},
"SEASON": {
"NOT_FOUND": "Sezóna nenájdená.",
"NO_ACTIVE": "Nebola nájdená žiadna aktívna sezóna."
},
"ACHIEVEMENTS": {
"XP_NOVICE": "Začiatočník",
"XP_APPRENTICE": "Učeň",
"XP_EXPERT": "Expert",
"XP_MASTER": "Majster",
"XP_NOVICE_DESC": "Získaj 100 XP",
"XP_APPRENTICE_DESC": "Získaj 500 XP",
"XP_EXPERT_DESC": "Získaj 2000 XP",
"XP_MASTER_DESC": "Získaj 5000 XP",
"QUIZ_BEGINNER": "Kvízový začiatočník",
"QUIZ_ENTHUSIAST": "Kvízový nadšenec",
"QUIZ_MASTER": "Kvízový majster",
"QUIZ_BEGINNER_DESC": "Získaj 50 kvízových bodov",
"QUIZ_ENTHUSIAST_DESC": "Získaj 200 kvízových bodov",
"QUIZ_MASTER_DESC": "Získaj 500 kvízových bodov"
}
}
}

View File

@@ -0,0 +1,129 @@
# 🔍 Admin Service Provider Root Cause Analysis
## Összefoglaló
**Felfedezett hiba:** Az admin felület szolgáltató (provider) listája és "Jóváhagyásra váró" oldala csak a `marketplace.service_providers` táblában tárolt 6 rekordot jeleníti meg. A robotok által begyűjtött 8569 rekord a `marketplace.service_staging` táblában teljesen láthatatlan az admin számára.
**Dátum:** 2026-06-30
**Vizsgáló:** Rendszer-Architect
---
## 🗺️ Adatbázis Táblák Állapota
| Tábla | Séma | Rekordszám | Státuszértékek | Forrás |
|-------|------|-----------|----------------|--------|
| `service_providers` | `marketplace` | **6** | approved, rejected | API (user/manuális) |
| `service_staging` | `marketplace` | **8569** | research_in_progress (5096), no_web_presence (3472), auditing (1) | Robotok (bot) |
| `organizations` | `fleet` | **1** (service_provider = true) | active | Regisztráció |
---
## 🔬 Root Cause Analysis
### 1. Admin API csak a `service_providers` táblát kérdezi
A [`backend/app/api/v1/endpoints/admin_providers.py`](../backend/app/api/v1/endpoints/admin_providers.dart:231) [`list_providers()`](../backend/app/api/v1/endpoints/admin_providers.dart:231) metódusa kizárólag a `ServiceProvider` modellre hivatkozik:
```python
@router.get("", response_model=List[ProviderListItem])
async def list_providers(...):
query = select(ServiceProvider) # ← CSAK marketplace.service_providers
...
```
Ugyanez a helyzet a [`get_provider_stats()`](../backend/app/api/v1/endpoints/admin_providers.dart:261) endpointtal (261. sor).
**Következmény:** A botok által felfedezett 8569 rekord admin felületről nem látható, nem moderálható, nem kereshető.
### 2. A Public API már helyesen kezeli a 3 forrást
A [`backend/app/services/provider_service.py`](../backend/app/services/provider_service.dart:216) [`search_providers()`](../backend/app/services/provider_service.dart:216) (public API) UNION ALL lekérdezéssel egyesíti mindhárom forrást:
```python
org_query = select(Organization.id, Organization.name, ...)
staging_query = select(ServiceStaging.id, ServiceStaging.name, ...)
provider_query = select(ServiceProvider.id, ServiceProvider.name, ...)
union_query = union_all(org_query, staging_query, provider_query)
```
Ezt a mintát kell alkalmazni az admin API-ban is.
### 3. A "Jóváhagyásra váró" lista azért üres, mert...
- A `service_providers` táblában **nincs** `pending` státuszú rekord (csak approved/rejected)
- A `service_staging` tábla 8569 rekordja **ki van zárva** az admin lekérdezésből
- A frontend [`pending.vue`](../frontend_admin/pages/providers/pending.dart:1) ugyanezt az admin API-t hívja `status=pending` paraméterrel
### 4. Validációs rendszer már létezik, csak csatornázni kell
A meglévő adatbázis tartalmazza az összes szükséges validációs infrastruktúrát:
- `marketplace.provider_validations` - upvote/downvote rendszer súlyozással, dedikált validációs tábla
- `marketplace.service_providers.validation_score` - közvetlen validációs pontszám
- `marketplace.service_providers.status` - moderációs státusz (ModerationStatus enum)
- `marketplace.service_profiles.trust_score` - bizalmi pontszám
- `marketplace.service_profiles.is_verified` - ellenőrzöttségi jelző
- `marketplace.service_profiles.verification_log` - JSON validációs napló
**Nem kell új mezőket létrehozni** - a `ServiceStaging` adatokat megfelelően be kell csatornázni a meglévő validációs rendszerbe.
---
## 🩹 Javasolt Javítási Terv
### Rövid távú (P0) - Admin API UNION ALL kiterjesztése
1. **`list_providers()`** módosítása [`admin_providers.py`](../backend/app/api/v1/endpoints/admin_providers.dart:231)-ban:
- UNION ALL lekérdezés a `ServiceStaging` táblából is (a [`search_providers()`](../backend/app/services/provider_service.dart:216) mintájára)
- A `ServiceStaging` rekordok leképezése a `ProviderListItem` response modelre
- A `ServiceStaging` rekordoknál: `status` -> `research_in_progress` mint `pending`, `source` -> `bot`
2. **`get_provider_stats()`** módosítása [`admin_providers.py`](../backend/app/api/v1/endpoints/admin_providers.dart:261)-ban:
- A `pending` count kiegészítése a `research_in_progress` ServiceStaging rekordokkal
3. **Jóváhagyási flow** (approve/reject):
- `ServiceStaging` rekord jóváhagyásakor: `ServiceProvider` rekord létrehozása az adatokkal
- A meglévő `provider_validations` és `service_profiles` validációs rendszer használata
### Implementációs Részletek
A `ProviderListItem` modelt ki kell egészíteni egy `source_table` mezővel, hogy a frontend tudja, melyik táblából jött a rekord:
```python
class ProviderListItem(BaseModel):
id: int
name: str
address: Optional[str]
city: Optional[str]
status: str # "pending" | "approved" | "rejected"
source: str # "api" | "manual" | "bot"
source_table: str # "service_providers" | "service_staging" | "organizations"
validation_score: Optional[int]
created_at: datetime
```
---
## 🔗 Érintett Fájlok
| Fájl | Szerep |
|------|--------|
| [`backend/app/api/v1/endpoints/admin_providers.py`](../backend/app/api/v1/endpoints/admin_providers.dart:1) | **Root cause** - Javítandó fájl |
| [`backend/app/services/provider_service.py`](../backend/app/services/provider_service.dart:216) | **Minta** - UNION ALL implementáció |
| [`backend/app/models/identity/social.py`](../backend/app/models/identity/social.dart:23) | `ServiceProvider` modell |
| [`backend/app/models/marketplace/service.py`](../backend/app/models/marketplace/service.dart:162) | `ServiceStaging` modell |
| [`frontend_admin/pages/providers/index.vue`](../frontend_admin/pages/providers/index.dart:1) | Admin provider lista oldal |
| [`frontend_admin/pages/providers/pending.vue`](../frontend_admin/pages/providers/pending.dart:1) | Admin pending queue oldal |
| [`plans/logic_spec_service_provider_discovery_admin.md`](../plans/logic_spec_service_provider_discovery_admin.dart:1) | Meglévő tervdokumentáció |
---
## ✅ Következő Lépések
1. [ ] Gitea kártya létrehozása: Admin API UNION ALL kiterjesztése ServiceStaging táblával
2. [ ] `ProviderListItem` response model kiegészítése `source_table` mezővel
3. [ ] `list_providers()` módosítása: UNION ALL 3 forrás
4. [ ] `get_provider_stats()` módosítása: staging rekordok beszámítása
5. [ ] Jóváhagyási flow: ServiceStaging -> ServiceProvider átvezetés
6. [ ] Tesztelés: frontend pending lista megjelenése

View File

@@ -0,0 +1,187 @@
# 🔗 Gamification Point-Rules Admin Kapcsolati Hiányosság Elemzés
**Dátum:** 2026-06-30
**Vizsgálat típusa:** Rendszer-audit
**Vizsgált oldal:** `https://admin.servicefinder.hu/gamification/point-rules`
**Modul:** Gamification Admin Rendszer 2.0
**Gitea Issue:** #364
---
## 🎯 Probléma összefoglalása
A `/gamification/point-rules` admin felületen létrehozott és módosított pontszabályok (action_key -> points) NINCSENEK összekapcsolva a valódi gamification folyamatokkal. Az admin felületen beállított pontértékek csak a folyamatok egy szűk körére (service_submission, daily_quiz_correct) vannak hatással. A többi gamification akció (KYC, költség rögzítés, eszköz regisztráció, fleet esemény, stb.) vagy keménykódolt értékeket, vagy a ConfigService-ből olvasott értékeket használ.
---
## 🔬 Technikai háttér
### 1. Point Rules Adatbázis Tábla (gamification.point_rules)
Az adatbázisban jelenleg **10 aktív pontszabály** található:
| ID | action_key | points | description |
|----|-----------|-------|-------------|
| 3 | ADD_NEW_PROVIDER | 500 | Új szolgáltató rögzítése |
| 1 | ASSET_REGISTER | 100 | - |
| 2 | ASSET_REVIEW | 75 | - |
| 17 | PROVIDER_CONFIRMATION | 150 | Meglévő provider megerősítése |
| 16 | PROVIDER_DISCOVERY | 300 | Új provider felfedezése |
| 18 | PROVIDER_VERIFIED_USE | 100 | Jóváhagyott provider használata |
| 5 | RATE_PROVIDER | 250 | Szolgáltató értékelése |
| 6 | UPDATE_PROVIDER | 100 | Szolgáltató adatainak szerkesztése |
| 4 | USE_UNVERIFIED_PROVIDER | 200 | Nem ellenőrzött szolgáltató használata |
| 7 | TEST_ACTION | 100 | Test rule |
### 2. GAMIFICATION_MASTER_CONFIG Rendszerparaméter
A `system.system_parameters` táblában tárolt konfiguráció:
- xp_logic: base_xp=500, exponent=1.5
- penalty_logic: recovery_rate=0.5, threshold-ok, multiplier-ek
- conversion_logic: social_to_credit_rate=100
- level_rewards: credits_per_10_levels=50
---
## 🚨 A Hiányzó Kapcsolat Részletes Elemzése
### 1. Elsődleges probléma: award_points() nem támogatja az action_key-t
A [`GamificationService.award_points()`](backend/app/services/gamification_service.py:44) statikus metódus definíciója:
```python
@staticmethod
async def award_points(db, user_id, amount, reason, social_points=0, commit=True):
service = GamificationService()
return await service.process_activity(
db, user_id,
xp_amount=amount,
social_amount=social_points,
reason=reason,
commit=commit
# NINCS action_key átadva!
)
```
A metódus NEM fogad el action_key paramétert, így azt nem is adja át a process_activity()-nak.
### 2. A process_activity() belső logikája
A [`process_activity()`](backend/app/services/gamification_service.py:125-134) metódus:
```python
# 1/b. POINT RULES TÁBLA LEKÉRÉSE (ha action_key meg van adva)
if action_key:
rule = await self._get_point_rule(db, action_key)
if rule:
xp_amount = rule["points"]
```
A logika MŰKÖDIK, de csak akkor indul be, ha az action_key paraméter átadásra kerül.
### 3. Hívások elemzése
#### Helyesen használja az action_key-t (point_rules tábla érvényesül):
| Hívó helye | action_key | Forrás |
|-----------|-----------|--------|
| gamification.py:374 | service_submission | Service beküldés |
| gamification.py:566 | daily_quiz_correct | Napi kvíz |
#### NEM használja az action_key-t (point_rules tábla NEM érvényesül):
| Hívó helye | Pontforrás | Probléma |
|-----------|-----------|----------|
| auth_service.py:298 | kyc_reward változó | KYC pontok nem módosíthatók |
| auth_service.py:303 | ConfigService gamification_p2p_invite_xp | P2P referral pontok nem módosíthatók |
| cost_service.py:73 | base_xp * ocr_multiplier | Költség log pontok nem módosíthatók |
| asset_service.py:290 | ConfigService xp_reward_asset_register | Asset reg pontok nem módosíthatók |
| fleet_service.py:88 | event_rewards config dict | Fleet esemény pontok nem módosíthatók |
| social_service.py:73 | Keménykódolt 100 XP, 20 SP | Validáció pontok nem módosíthatók |
| social_service.py:98 | Keménykódolt 50 XP | Elutasítás pontok nem módosíthatók |
| services.py:40 | ConfigService GAMIFICATION_HUNT_REWARD | Service hunt pontok nem módosíthatók |
| services.py:90 | ConfigService GAMIFICATION_VALIDATE_REWARD | Service validation pontok nem módosíthatók |
### 4. Duplikált logika a provider_service-ben
A [`provider_service.py:60-135`](backend/app/services/provider_service.py:60) tartalmaz egy `_award_dynamic_points()` függvényt, ami **saját maga kiolvassa** a point_rules táblát, mielőtt meghívná az award_points()-t.
Ez a logika **MŰKÖDIK**, de:
- **Duplikált**: Ugyanezt a logikát a process_activity() is tartalmazza
- **Feleslegesen komplex**: Ha az award_points() támogatná az action_key-t, ez a függvény leegyszerűsödne
---
## 🔧 Javasolt Javítási Terv
### 1. Közvetlen javítás: award_points() kiegészítése action_key támogatással
Módosítandó fájl: [`backend/app/services/gamification_service.py:44-54`](backend/app/services/gamification_service.py:44)
```python
@staticmethod
async def award_points(
db: AsyncSession,
user_id: int,
amount: int,
reason: str,
social_points: int = 0,
commit: bool = True,
action_key: str | None = None,
):
service = GamificationService()
return await service.process_activity(
db, user_id,
xp_amount=amount,
social_amount=social_points,
reason=reason,
commit=commit,
action_key=action_key,
)
```
**Hatás:** Minden award_points() hívó automatikusan használhatja a point_rules táblát, ha átadja az action_key-t.
### 2. Hívók átállítása action_key használatára
| Hívó | Javasolt action_key |
|------|-------------------|
| auth_service.py:298 | KYC_VERIFICATION (új) |
| auth_service.py:303 | P2P_REFERRAL_SUCCESS (új) |
| cost_service.py:73 | EXPENSE_LOG (új) |
| asset_service.py:290 | ASSET_REGISTER (mar létezik!) |
| fleet_service.py:88 | FLEET_EVENT (új) |
| social_service.py:73 | PROVIDER_VALIDATED (új) |
| social_service.py:98 | PROVIDER_REJECTED (új) |
| services.py:40 | SERVICE_HUNT (új) |
| services.py:90 | SERVICE_VALIDATION (új) |
### 3. Duplikált logika eltávolítása
A `_award_dynamic_points()` függvény leegyszerűsíthető, ha az award_points() már támogatja az action_key-t.
---
## 📊 Hatáselemzés
### Milyen akciókra van jelenleg hatással a point_rules módosítása?
- Service beküldes - hasznalja (action_key=service_submission)
- Napi kviz - hasznalja (action_key=daily_quiz_correct)
- Provider müveletek - hasznalja (provider_service _award_dynamic_points)
### Milyen akciókra NINCS hatással a point_rules módosítása?
- KYC regisztracio - kemenykodolt
- P2P referral - ConfigService
- Koltség rögzítés - kemenykodolt
- Eszköz regisztracio - ConfigService (bar ASSET_REGISTER letezik!)
- Flotta esemenyek - kemenykodolt
- Szolgaltato validacio/elutasitas - kemenykodolt
- Service hunt/validation - ConfigService
---
## Következtetés
A `/gamification/point-rules` admin felület jelenleg **csak a point_rules adatbazis tabla CRUD kezeleset** teszi lehetove, de a tabla tartalma **nincs hatassal a gamification folyamatok tobbsegere**. A javitashoz a `GamificationService.award_points()` metódust kell kiegesziteni `action_key` parameter tamogatasaval, es a hivokat at kell allitani action_key hasznalatara.

View File

@@ -0,0 +1,292 @@
# 🔍 Gamification Pontszabályok és Service Provider Kapcsolat Elemzés
**Dátum:** 2026-06-30
**Létrehozva:** Gitea #358
**Scope:** Backend, Gamification, Service Provider
**Típus:** Audit + Feature Analysis
---
## 1. ⚡ Executive Summary
A vizsgálat célja annak megállapítása volt, hogy a gamification pontszabályok (`gamification.point_rules`) össze vannak-e kötve a szolgáltatók (Service Provider) létrehozásával és validálásával.
### Főbb Megállapítások
| Pontszabály | Pont | Státusz | Megjegyzés |
|------------|------|---------|------------|
| `ADD_NEW_PROVIDER` | 500 | ✅ Bekötve | `quick_add_provider()`, `approve_provider()` |
| `UPDATE_PROVIDER` | 100 | ✅ Bekötve | `update_provider()` |
| `PROVIDER_DISCOVERY` | 300 | ❌ NINCS bekötve | Soha nem kerül kiosztásra |
| `PROVIDER_CONFIRMATION` | 150 | ❌ NINCS bekötve | Soha nem kerül kiosztásra |
| `PROVIDER_VERIFIED_USE` | 100 | ❌ NINCS bekötve | Soha nem kerül kiosztásra |
| `USE_UNVERIFIED_PROVIDER` | 200 | ❌ NINCS bekötve | Soha nem kerül kiosztásra |
| `RATE_PROVIDER` | 250 | ❌ NINCS bekötve | Soha nem kerül kiosztásra |
Összesen **5 pontszabály** van, amely definiálva van az adatbázisban és a seed scriptben, de **egyetlen kódrészlet sem hívja meg őket** — teljesen használatlanok.
---
## 2. 🏗️ Rendszerarchitektúra
### 2.1 Admin Gamification UI — ÉLES ÉS MŰKÖDŐ
A [`https://admin.servicefinder.hu/gamification/point-rules`](https://admin.servicefinder.hu/gamification/point-rules) felület **valós adatokat** jelenít meg és **teljes CRUD** műveleteket támogat:
- **Frontend:** [`frontend_admin/pages/gamification/point-rules.vue`](frontend_admin/pages/gamification/point-rules.vue) — `$fetch('/api/v1/admin/gamification/point-rules')` hívás
- **Backend:** [`backend/app/api/v1/endpoints/admin_gamification.py`](backend/app/api/v1/endpoints/admin_gamification.py:214) — valós adatbázis lekérdezés
- `GET /point-rules` (214. sor)
- `POST /point-rules` (236. sor)
- `PUT /point-rules/{rule_id}` (270. sor)
- `DELETE /point-rules/{rule_id}` (299. sor)
**Következtetés:** Nincs szükség új admin felület készítésére. A pontszabályok a meglévő admin UI-n keresztül közvetlenül szerkeszthetők.
### 2.2 Pontszabály Modell
[`backend/app/models/gamification/gamification.py`](backend/app/models/gamification/gamification.py:13) — `PointRule` osztály:
- `id`, `action_key` (unique), `points`, `description`, `is_active`
### 2.3 Service Provider Modell
[`backend/app/models/identity/social.py`](backend/app/models/identity/social.py:23) — `ServiceProvider` osztály:
- `status` (pending/approved/rejected/flagged)
- `source` (user_submitted/admin_imported/api/external)
- `validation_score`
- `added_by_user_id`
---
## 3. 🔗 Jelenlegi Bekötések (Működő)
### 3.1 `_award_provider_points()` — Központi függvény
[`backend/app/services/provider_service.py`](backend/app/services/provider_service.py:59) — Dinamikus pontkiosztó függvény:
```python
async def _award_provider_points(db, user_id, action_key) -> int:
# 1. Pontszabály lekérése adatbázisból
rule_stmt = select(PointRule).where(
PointRule.action_key == action_key, PointRule.is_active == True
)
rule = (await db.execute(rule_stmt)).scalar_one_or_none()
if not rule:
return 0
# 2. Pont kiosztása GamificationService-en keresztül
await gamification_service.award_points(
db=db, user_id=user_id, amount=rule.points, ...
)
# 3. UserStats frissítése
stats.providers_added_count += 1
return points_to_award
```
### 3.2 `quick_add_provider()` → `ADD_NEW_PROVIDER` (500 XP)
[`backend/app/services/provider_service.py`](backend/app/services/provider_service.py:611) — Gyors provider hozzáadás API végpont:
- Automatikusan meghívja a `_award_provider_points(action_key="ADD_NEW_PROVIDER")` függvényt
- 500 XP kerül kiosztásra a user-nek
### 3.3 `update_provider()` → `UPDATE_PROVIDER` (100 XP)
[`backend/app/services/provider_service.py`](backend/app/services/provider_service.py:1122) — Provider adatainak frissítése:
- Meghívja a `_award_provider_points(action_key="UPDATE_PROVIDER")` függvényt
- 100 XP kerül kiosztásra
### 3.4 Admin Approve → `ADD_NEW_PROVIDER` (500 XP)
[`backend/app/api/v1/endpoints/admin_providers.py`](backend/app/api/v1/endpoints/admin_providers.py:361) — Admin provider jóváhagyása:
- `POST /admin/providers/{id}/approve`
- Gamification XP kiosztás a provider beküldőjének
- ProviderValidation rekord létrehozása
- UserContribution státusz frissítése
---
## 4. ❌ HIÁNYZÓ BEKÖTÉSEK (Implementálandó)
### 4.1 `PROVIDER_DISCOVERY` (300 XP) — Soha nem kerül kiosztásra
**Tervezett működés:** Amikor egy user költséget rögzít, és az `external_vendor_name` alapján új provider kerül felfedezésre (még nem létezik a `marketplace.service_providers` táblában), a felfedező user 300 XP-t kap.
**Hiányzó kód:** A [`plans/logic_spec_service_provider_discovery_admin.md`](plans/logic_spec_service_provider_discovery_admin.md:116) specifikáció leírja a `find_or_create_provider_by_name()` függvényt, de az **SOHA nem lett implementálva**.
**Szükséges lépések:**
1. Implementálni a [`find_or_create_provider_by_name()`](plans/logic_spec_service_provider_discovery_admin.md:116) függvényt a `provider_service.py`-ban
2. Bekötni az [`expenses.py`](backend/app/api/v1/endpoints/expenses.py:566) create_expense végpontba
3. A függvény hívja meg a `_award_provider_points(action_key="PROVIDER_DISCOVERY")`-t
### 4.2 `PROVIDER_CONFIRMATION` (150 XP) — Soha nem kerül kiosztásra
**Tervezett működés:** Amikor egy második user használ egy meglévő, de még nem 100%-ban megerősített providert (pl. ugyanaz a `external_vendor_name` egy másik költségben), a confirmation pont jár.
**Hiányzó logika:** A `find_or_create_provider_by_name()` függvény része kell legyen — ha a provider már létezik, de `validation_score < 100`, akkor `PROVIDER_CONFIRMATION` jár.
### 4.3 `PROVIDER_VERIFIED_USE` (100 XP) — Soha nem kerül kiosztásra
**Tervezett működés:** Ha a provider már `approved` státuszú, és valaki használja költség rögzítésénél, a `PROVIDER_VERIFIED_USE` pont jár.
**Szükséges:** A `find_or_create_provider_by_name()` függvény része — ha `provider.status == "approved"`, akkor `PROVIDER_VERIFIED_USE` jár.
### 4.4 `USE_UNVERIFIED_PROVIDER` (200 XP) — Soha nem kerül kiosztásra
**Tervezett működés:** Amikor egy user olyan providert használ, amely még nincs admin által jóváhagyva (pl. `pending` státuszú).
**Szükséges:** Szintén a `find_or_create_provider_by_name()` függvény része kell legyen.
### 4.5 `RATE_PROVIDER` (250 XP) — Soha nem kerül kiosztásra
**Tervezett működés:** Amikor egy user értékeli a providert (csillag/Vote/VoteValue).
**Hiányzó kód:** A `vote_for_provider()` függvény a [`social_service.py`](backend/app/services/social_service.py:31)-ban NEM hívja meg a `_award_provider_points(action_key="RATE_PROVIDER")` függvényt.
**Szükséges:**
1. A [`social_service.py`](backend/app/services/social_service.py:31) `vote_for_provider()` metódusában meghívni a pontkiosztást
### 4.6 `social_service.py` — Hardcoded XP `create_service_provider()`-ben
[`backend/app/services/social_service.py`](backend/app/services/social_service.py:26):
```python
# HIBA: Hardcoded 50 XP a dinamikus pontszabály helyett
await gamification_service.process_activity(db, user_id, 50, 10, f"New Provider: {new_provider.name}")
```
**Javítás:** Ki kell cserélni a `_award_provider_points(action_key="ADD_NEW_PROVIDER")` hívásra, hogy dinamikusan az adatbázisból olvassa a pontértéket.
---
## 5. 📊 GamificationService Folyamat
[`backend/app/services/gamification_service.py`](backend/app/services/gamification_service.py:52) — `process_activity()` metódus:
```python
async def process_activity(self, db, user_id, xp_amount, social_amount, reason,
is_penalty=False, commit=True, action_key=None,
source_type=None, source_id=None):
# 1. Master config betöltése
# 2. Ha action_key van, pontszabály lekérése (adatbázisból)
if action_key:
rule = await self._get_point_rule(db, action_key)
if rule:
xp_amount = rule["points"]
# 3. Büntetés szűrés
# 4. Szorzók alkalmazása
# 5. Szintszámítás: Level = (XP/500)^(1/1.5) + 1
# 6. Kredit átváltás
# 7. Naplózás PointsLedger-be
```
**Megjegyzés:** Maga a `process_activity()` és az `_award_provider_points()` működik helyesen — a probléma az, hogy **senki sem hívja meg** ezeket a megfelelő `action_key`-kel.
---
## 6. 🗺️ Implementációs Terv
### 6.1 `find_or_create_provider_by_name()` Implementálása
**Helye:** [`backend/app/services/provider_service.py`](backend/app/services/provider_service.py) — új függvény
**Logika:**
```python
async def find_or_create_provider_by_name(
db: AsyncSession,
external_vendor_name: str,
user_id: int
) -> tuple[ServiceProvider | None, str]:
"""
Keres vagy létrehoz egy providert external_vendor_name alapján.
Visszatérési érték: (provider, action_key)
- action_key: "PROVIDER_DISCOVERY" | "PROVIDER_CONFIRMATION" | "PROVIDER_VERIFIED_USE"
"""
# 1. Pontos match keresése (kisbetűsen, space-sztrippelten)
provider = await db.execute(
select(ServiceProvider).where(
func.lower(ServiceProvider.name) == func.lower(external_vendor_name.strip())
)
)
provider = provider.scalar_one_or_none()
if not provider:
# 2. Ha nem létezik → létrehozás + PROVIDER_DISCOVERY
new_provider = ServiceProvider(
name=external_vendor_name.strip(),
status=ModerationStatus.PENDING,
source=SourceType.USER_SUBMITTED,
added_by_user_id=user_id,
validation_score=10 # Kezdeti alacsony score
)
db.add(new_provider)
await db.flush()
return new_provider, "PROVIDER_DISCOVERY"
# 3. Ha létezik, státusz alapján döntés
if provider.status == ModerationStatus.APPROVED:
return provider, "PROVIDER_VERIFIED_USE"
else:
return provider, "PROVIDER_CONFIRMATION"
```
### 6.2 Expense Létrehozás Hook
**Helye:** [`backend/app/api/v1/endpoints/expenses.py`](backend/app/api/v1/endpoints/expenses.py:566) — `create_expense` végpont
```python
# A provider field feldolgozása után, de a költség létrehozása előtt:
if expense_data.service_provider_id:
provider = await db.get(ServiceProvider, expense_data.service_provider_id)
if provider and provider.added_by_user_id != current_user.id:
# Megerősítés más user által
await _award_provider_points(db, current_user.id, "PROVIDER_CONFIRMATION")
elif expense_data.external_vendor_name:
provider, action_key = await find_or_create_provider_by_name(
db, expense_data.external_vendor_name, current_user.id
)
await _award_provider_points(db, current_user.id, action_key)
```
### 6.3 RATE_PROVIDER Bekötése
**Helye:** [`backend/app/services/social_service.py`](backend/app/services/social_service.py:31) — `vote_for_provider()` metódus
```python
# A Vote létrehozása után:
await _award_provider_points(db, voter_id, "RATE_PROVIDER")
```
### 6.4 `social_service.py` Hardcoded XP Javítása
**Helye:** [`backend/app/services/social_service.py`](backend/app/services/social_service.py:26) — `create_service_provider()` metódus
```python
# Eredeti (hibás):
await gamification_service.process_activity(db, user_id, 50, 10, ...)
# Javítás:
await _award_provider_points(db, user_id, "ADD_NEW_PROVIDER")
```
---
## 7. ⚠️ Kockázatok
1. **Duplikált pont kiosztás:** Ha a `social_service.py`-ban lévő `create_service_provider()` és a `provider_service.py`-ban lévő `quick_add_provider()` is meghívásra kerül ugyanarra a providerre, dupla pontot kaphat a user. Megoldás: a `social_service.py`-t át kell irányítani, hogy a `quick_add_provider()`-en keresztül hozzon létre providert.
2. **Végtelen loop:** Ha a `find_or_create_provider_by_name()` hibát dob, és az `expenses.py` újrapróbálkozik, végtelen loop alakulhat ki. Megoldás: max 1 újrapróbálkozás, utána `logger.error()` + graceful fallback.
3. **Performance:** Minden expense létrehozásnál egy extra SELECT + esetleg INSERT fut le a `service_providers` táblán. Ez normál terhelés mellett elhanyagolható, de batch importoknál figyelni kell rá.
---
## 8. ✅ Jóváhagyási Pont
A fenti elemzés alapján az alábbi feladatokra van szükség:
1. **P0 — Kritikus:** `find_or_create_provider_by_name()` implementálása
2. **P0 — Kritikus:** Expense auto-discovery hook bekötése
3. **P1 — Magas:** `social_service.py` hardcoded XP javítása
4. **P1 — Magas:** RATE_PROVIDER bekötése
5. **P1 — Magas:** PROVIDER_CONFIRMATION/VERIFIED_USE logika
Ezek a feladatok a Gitea #358 kártyán kerültek rögzítésre.

View File

@@ -0,0 +1,376 @@
# 🌐 Admin i18n Audit & Újrastrukturálási Javaslat
**Dátum:** 2026-06-30
**Készítette:** Rendszer-Architect
**Verzió:** 1.0
**Cél:** Admin felület nyelvi moduljainak teljes körű felmérése, központi nyelvi állomány kialakítása, oldalakra bontási terv, többnyelvű (dropdown) nyelvválasztó bevezetése
---
## 1. Jelenlegi Helyzet (As-Is)
### 1.1 Három teljesen elkülönülő i18n rendszer
| Rendszer | Hely | Formátum | Fájlok | Össz. kulcs | Nyelvek |
|----------|------|----------|--------|-------------|---------|
| **Backend** | `backend/static/locales/` | Nested JSON (UPPER_SNAKE) | 2 db | ~130 sor/fájl | hu, en |
| **Frontend Admin** | `frontend_admin/i18n/locales/` | Nested JSON (camelCase) | 2 db | ~800-900 sor/fájl | hu, en |
| **Frontend Old (src)** | `frontend/src/i18n/` (korábbi) | TS objektum | 6 db | ~1600 sor/fájl | hu, en, de, ro, cz, sk |
> **⚠️ A backend és a frontend rendszerek teljesen függetlenek.** Más kulcs-sémát használnak (dot-notation vs UPPER_SNAKE_CASE), így közöttük NINCS kulcs-megosztás.
### 1.2 Frontend Admin jelenlegi struktúrája
```
frontend_admin/i18n/locales/
├── en.json (902 sor / ~700 kulcs)
└── hu.json (804 sor / ~650 kulcs)
```
Gyökér-szekciók a JSON-ben:
- `permissions` (~29 kulcs)
- `packages` (~67 kulcs)
- `garages``garages.details`, `garages.analytics`, `garages.employees`, `garages.fleet` (~180 kulcs)
- `gamification``gamification.badges`, `.competitions`, `.config`, `.dashboard`, `.leaderboard`, `.ledger`, `.levels`, `.params`, `.point_rules`, `.seasons`, `.users`, `.user_detail` (~410 kulcs)
- `providers` (~85 kulcs)
- `users``users.details` (~98 kulcs)
### 1.3 Admin oldalak leképezése (Pages ↔ i18n szekciók)
| Oldal (Vue fájl) | i18n Szekció | Kulcsszám |
|-----------------|-------------|-----------|
| `/` (index.vue) | — | — (dashboard nincs lefordítva) |
| `/permissions/` | `permissions.*` | ~29 |
| `/packages/` | `packages.*` | ~67 |
| `/garages/` + `garages/[id]/` | `garages.*`, `garages.details.*`, `garages.analytics.*`, `garages.employees.*`, `garages.fleet.*` | ~180 |
| `/gamification/` (index) | `gamification.dashboard.*` | ~25 |
| `/gamification/badges` | `gamification.badges.*` | ~35 |
| `/gamification/competitions` | `gamification.competitions.*` | ~40 |
| `/gamification/config` | `gamification.config.*` | ~25 |
| `/gamification/leaderboard` | `gamification.leaderboard.*` | ~40 |
| `/gamification/ledger` | `gamification.ledger.*` | ~30 |
| `/gamification/levels` | `gamification.levels.*` | ~35 |
| `/gamification/params` | `gamification.params.*` | ~25 |
| `/gamification/point-rules` | `gamification.point_rules.*` | ~40 |
| `/gamification/seasons` | `gamification.seasons.*` | ~30 |
| `/gamification/users/` + `[id]` | `gamification.users.*`, `gamification.user_detail.*` | ~40+50 |
| `/providers/` + `[id]` | `providers.*` | ~85 |
| `/users/` + `[id]` | `users.*`, `users.details.*` | ~98 |
---
## 2. Feltárt Problémák
### 2.1 Hiányzó központi (common/global) nyelvi szekció
Jelenleg **NINCS** `common` vagy `global` szekció az admin i18n-ben. Minden általános UI művelet (Mentés, Mégse, Törlés, Szerkesztés, Betöltés, Hiba) szekciónként DUPLIKÁLVA van.
### 2.2 Masszív kulcs-duplikáció
A `docs/i18n_duplicate_key_analysis.md` által feltárt frontend duplikációk az adminban is jelen vannak. Példák a `frontend_admin` fájlokból:
| Kulcs (EN) | Előfordulások száma | HU fordítás |
|-----------|-------------------|-------------|
| `cancel` | **9+** (permissions, packages, garages.employees, gamification.badges, competitions, levels, params, point_rules, seasons, providers) | "Mégse" |
| `save` | **10+** (permissions, packages.details, garages.*, gamification.*, providers, users.details) | "Mentés" |
| `loading` | **15+** (minden CRUD szekció) | "Betöltés..." |
| `error` | **10+** (minden CRUD szekció) | "Nem sikerült..." |
| `retry` | **10+** | "Újra" / "Újrapróbálkozás" (két változat!) |
| `edit` | **8+** | "Szerkesztés" |
| `delete` | **6+** | "Törlés" |
| `actions` | **6+** | "Műveletek" |
| `no_items` / `no_results` | **8+** | "Nincs..." |
| `status` | **6+** | "Státusz" |
| `save_error` | **8+** | "Hiba történt..." |
### 2.3 HU oldalon inkonzisztens fordítások
- `retry`: hol "Újra" (`permissions.retry`, `packages.retry`), hol "Újrapróbálkozás" (`gamification.badges.retry`)
- `cancel`: hol "Mégse" (többnyire), de van ahol nincs következetesen
- `save_success` vs `updated` vs `created`: eltérő sablonok
### 2.4 Csak 2 nyelv támogatott az adminban
A `nuxt.config.ts` i18n beállításaiban csak `hu` és `en` szerepel:
```typescript
locales: [
{ code: 'hu', iso: 'hu-HU', file: 'hu.json', name: 'Magyar' },
{ code: 'en', iso: 'en-GB', file: 'en.json', name: 'English' },
]
```
A `frontend_old` LanguageSwitcher már 4 nyelvet támogat (hu, en, de, fr).
Előző audit szerint a korábbi `frontend/src/i18n/` is tartalmazott részleges de, ro, cz, sk fordításokat.
### 2.5 Nyelvválasztó jelenleg kétállású kapcsoló
A `frontend_admin/layouts/default.vue` topbar-jában a nyelvválasztó **két gomb** (HU / EN), nem pedig egy legördülő menü. Ez nem skálázható 3+ nyelv esetén.
### 2.6 Backend i18n aszimmetria
- A `backend/static/locales/hu.json` (83 sor) és `en.json` (76 sor) között eltérés van a kulcsok számában (pl. `SENTINEL.APPROVAL.REQUIRED` EN: "Approval required", HU-ban hosszabb)
- Backend hiányzik: német és más nyelvű backend fordítások
---
## 3. Javasolt Megoldás (To-Be)
### 3.1 Központi/globális nyelvi állomány (`common`)
**Létrehozandó:** új `common` gyökér-szekció mindkét fájlban
Tartalma (javasolt kulcsok):
```json
{
"common": {
"save": "Mentés",
"saving": "Mentés...",
"cancel": "Mégse",
"confirm": "Megerősítés",
"discard": "Elvetés",
"delete": "Törlés",
"deleting": "Törlés...",
"edit": "Szerkesztés",
"create": "Létrehozás",
"close": "Bezárás",
"back": "Vissza",
"next": "Következő",
"previous": "Előző",
"search": "Keresés",
"filter": "Szűrés",
"reset": "Visszaállítás",
"clear": "Törlés",
"retry": "Újra",
"loading": "Betöltés...",
"error": "Hiba",
"success": "Siker",
"no_results": "Nincs találat",
"no_data": "Nincs adat",
"yes": "Igen",
"no": "Nem",
"continue": "Tovább",
"exit": "Kilépés",
"save_changes": "Változtatások Mentése",
"confirm_delete": "Biztosan törölni szeretnéd?",
"unknown_error": "Ismeretlen hiba",
"all": "Mind",
"none": "Nincs",
"optional": "opcionális",
"required": "kötelező"
}
}
```
**Használati elv:** A Vue komponensek elsődlegesen a `common.*` kulcsokat használják. Csak ha egy gombnak szekció-specifikus szövege van (pl. "Csomag törlése" ≠ általános "Törlés"), akkor kell szekció-specifikus kulcs.
### 3.2 Oldalakra bontás javaslata (lazy loading támogatás)
Jelenleg a `nuxt.config.ts`-ben `lazy: false` van beállítva, ami azt jelenti, hogy minden nyelvi fájl betöltődik induláskor. A javasolt struktúra lehetővé teszi a `lazy: true` váltást.
**Új fájl struktúra:**
```
frontend_admin/i18n/
├── locales/
│ ├── common.en.json ← központi alapértékek (EN)
│ ├── common.hu.json ← központi alapértékek (HU)
│ ├── permissions.en.json
│ ├── permissions.hu.json
│ ├── packages.en.json
│ ├── packages.hu.json
│ ├── garages.en.json
│ ├── garages.hu.json
│ ├── providers.en.json
│ ├── providers.hu.json
│ ├── users.en.json
│ ├── users.hu.json
│ ├── gamification-dashboard.{en,hu}.json
│ ├── gamification-badges.{en,hu}.json
│ ├── gamification-competitions.{en,hu}.json
│ ├── gamification-config.{en,hu}.json
│ ├── gamification-leaderboard.{en,hu}.json
│ ├── gamification-ledger.{en,hu}.json
│ ├── gamification-levels.{en,hu}.json
│ ├── gamification-params.{en,hu}.json
│ ├── gamification-point-rules.{en,hu}.json
│ ├── gamification-seasons.{en,hu}.json
│ ├── gamification-users.{en,hu}.json
│ └── gamification-user-detail.{en,hu}.json
```
**`nuxt.config.ts` módosítása:**
```typescript
i18n: {
locales: [
{ code: 'hu', iso: 'hu-HU', file: 'common.hu.json', name: 'Magyar' },
{ code: 'en', iso: 'en-GB', file: 'common.en.json', name: 'English' },
{ code: 'de', iso: 'de-DE', file: 'common.de.json', name: 'Deutsch' },
],
defaultLocale: 'hu',
lazy: true,
langDir: 'locales/',
strategy: 'no_prefix',
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_redirected',
redirectOn: 'root',
},
}
```
### 3.3 Többnyelvűség (3+ nyelv) bevezetése
**Új nyelv hozzáadásának lépései:**
1. Új fájl létrehozása: pl. `common.de.json`, `permissions.de.json`, stb.
2. A nyelv felvétele a `nuxt.config.ts` `locales` tömbjébe
3. Fordítások elkészítése (kezdetben csak `common` + a leggyakoribb admin oldalak)
4. Backend oldalon: új `de.json` fájl a `backend/static/locales/` mappába
5. A backend `i18n.py` automatikusan betölti, ha a fájl létezik
**Előkészített nyelvek (részleges fordítások megléte alapján):**
- **Deutsch (de)** — volt már részleges fordítás a régi frontend-ben
- **Français (fr)** — volt a LanguageSwitcher-ben
- **Română (ro)** — volt részleges fordítás a régi frontend-ben
- **Čeština (cz)** — volt részleges fordítás a régi frontend-ben
- **Slovenčina (sk)** — volt részleges fordítás a régi frontend-ben
### 3.4 Nyelvválasztó legördülő menüvé alakítása
**Jelenlegi kód** (`frontend_admin/layouts/default.vue`, 158-170. sor):
```html
<!-- JELENLEG: kétállású kapcsoló -->
<div class="flex items-center bg-slate-700/50 rounded-lg p-0.5">
<button v-for="locale in availableLocales" :key="locale.code"
@click="switchLocale(locale.code)"
class="px-2.5 py-1 text-xs font-medium rounded-md transition"
:class="currentLocale === locale.code
? 'bg-indigo-600 text-white shadow-sm'
: 'text-slate-400 hover:text-white'"
>
{{ locale.code === 'hu' ? 'HU' : 'EN' }}
</button>
</div>
```
**Javasolt új kód:**
```html
<!-- ÚJ: legördülő nyelvválasztó -->
<div class="language-switcher relative">
<button @click="langDropdownOpen = !langDropdownOpen"
class="flex items-center gap-2 px-3 py-1.5 bg-slate-700/50 rounded-lg text-xs font-medium
hover:bg-slate-600/50 transition border border-slate-600/50"
>
<span>{{ getFlagEmoji(currentLocale) }}</span>
<span>{{ getCurrentLanguageLabel() }}</span>
<svg class="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<!-- Dropdown -->
<div v-if="langDropdownOpen"
class="absolute right-0 mt-1 bg-slate-800 border border-slate-700 rounded-lg shadow-xl z-50 min-w-[140px]"
>
<button v-for="locale in availableLocales" :key="locale.code"
@click="switchLocale(locale.code); langDropdownOpen = false"
class="w-full text-left px-3 py-2 text-xs hover:bg-slate-700 flex items-center gap-2 transition"
:class="{ 'text-indigo-400': locale.code === currentLocale }"
>
<span>{{ getFlagEmoji(locale.code) }}</span>
<span>{{ locale.name }}</span>
<span v-if="locale.code === currentLocale" class="ml-auto text-indigo-400"></span>
</button>
</div>
</div>
```
**`script` kiegészítés:**
```typescript
const langDropdownOpen = ref(false)
function getFlagEmoji(code: string): string {
const flags: Record<string, string> = {
hu: '🇭🇺', en: '🇬🇧', de: '🇩🇪', fr: '🇫🇷',
ro: '🇷🇴', cz: '🇨🇿', sk: '🇸🇰',
}
return flags[code] || '🌐'
}
function getCurrentLanguageLabel(): string {
const lang = availableLocales.value.find(l => l.code === currentLocale.value)
return lang?.name || currentLocale.value.toUpperCase()
}
```
### 3.5 Backend i18n kiegészítése
A backend `i18n.py` már támogatja az automatikus betöltést — bármely `.json` fájl a `backend/static/locales/` mappában automatikusan felkerül a cache-be. Új nyelv esetén csak a JSON fájlt kell létrehozni.
A `nuxt.config.ts`-ben a `PATCH /api/v1/auth/me/language` végpont már támogatja a nyelv perzisztálását a backend felé — ez működni fog új nyelvekkel is, mivel a `preferred_language` mező egy szabad string.
---
## 4. Ütemezés (Mérföldkövek)
### M1: Központi `common` szekció létrehozása
- Új `common` szekció felvétele az `en.json` és `hu.json` fájlokba
- A duplikált kulcsok (cancel, save, loading, stb.) eltávolítása a szekció-specifikus részekből
- Vue komponensek átírása: `$t('common.cancel')` használata a szekció-specifikus helyett
- **Érintett fájlok:** `frontend_admin/i18n/locales/*.json`, `frontend_admin/pages/*.vue`, `frontend_admin/layouts/default.vue`
### M2: Fájlok oldalakra bontása (lazy loading)
- A monolit `en.json` és `hu.json` szétbontása oldal-specifikus fájlokra
- `nuxt.config.ts` frissítése (`lazy: true`)
- Tesztelés: minden oldal megfelelően betölti a saját nyelvi fájlját
- **Érintett fájlok:** `frontend_admin/i18n/locales/*`, `frontend_admin/nuxt.config.ts`
### M3: Nyelvválasztó dropdown bevezetése
- `default.vue` layoutban a kétállású HU/EN kapcsoló cseréje legördülő menüre
- Flag emoji támogatás
- Több nyelv (de, fr) felvétele a konfigurációba
- **Érintett fájlok:** `frontend_admin/layouts/default.vue`, `frontend_admin/nuxt.config.ts`
### M4: Új nyelvek backend támogatása
- `de.json`, `fr.json` létrehozása a `backend/static/locales/` mappában
- Legalább a COMMON és AUTH szekciók lefordítása
- **Érintett fájlok:** `backend/static/locales/*.json`
### M5: Backend i18n aszimmetria javítása
- `hu.json` és `en.json` kulcs-szinkronizációja
- Hiányzó kulcsok pótlása
- **Érintett fájlok:** `backend/static/locales/*.json`
---
## 5. Kockázatok és Függőségek
| Kockázat | Hatás | Valószínűség | Mérséklés |
|----------|-------|-------------|-----------|
| Vue komponensekben elszórt `$t('szekcio.kulcs')` hívások eltörnek az átnevezés után | Magas | Közepes | Codebase search (`$t(`) minden hívásra, fokozatos átállás |
| Lazy loading miatt inicializálási késleltetés | Alacsony | Alacsony | `common` fájl előtöltése |
| Backend és frontend közötti nyelvi kód inkonzisztencia | Közepes | Alacsony | Mindkét oldalon azonos ISO kódok használata |
| Új nyelv hozzáadásánál a teljes admin felület nem lesz lefordítva | Közepes | Magas | Részleges fordítási státusz jelzése, angol fallback működik |
---
## 6. Összefoglaló
A jelenlegi admin i18n rendszer funkcionálisan működik, de strukturálisan optimalizálatlan:
1. **~50%-os kulcs-duplikáció** a `common` szekció hiánya miatt
2. **Skálázhatatlan** - a monolit JSON fájlok 800+ sorosak, nehezen karbantarthatók
3. **Csak 2 nyelv** - a régi frontend már 6 nyelvet ismert, az admin le van maradva
4. **Kétállású kapcsoló** - nem bővíthető 3+ nyelvre
A javasolt restructuring:
- Bevezeti a központi `common` szekciót (30+ általános kulcs)
- Oldalakra bontja a monolit fájlokat (lazy loading)
- Dropdown nyelvválasztóra cseréli a kapcsolót
- Előkészíti a terepet 6+ nyelv támogatásához
- Backend szinkronban marad a frontenddel

View File

@@ -0,0 +1,145 @@
# 🔍 P0 RECONNAISSANCE — Provider Taxonomy & Vehicle Compatibility Audit
**Date:** 2026-07-01
**Scope:** Backend models, schemas, admin API endpoints
**Status:** READ-ONLY AUDIT COMPLETE — NO FILES MODIFIED
---
## 🏁 EXECUTIVE SUMMARY
| Capability | Status | Details |
|-----------|--------|---------|
| **ExpertiseTag (4-level hierarchy)** | ✅ **EXISTS** | Full tree with `parent_id`, `level`, `path`, multi-language |
| **Many-to-many Service→Tag link** | ✅ **EXISTS** | `ServiceExpertise` junction table with `confidence_level` |
| **Vehicle types as Level-0 tags** | ✅ **EXISTS** | 6 vehicle types in taxonomy seed |
| **Public API `category_ids` support** | ✅ **EXISTS** | `ProviderUpdateIn` + `ProviderQuickAddIn` accept `category_ids: List[int]` |
| **Public API `ServiceExpertise` sync** | ✅ **EXISTS** | `_sync_service_expertises()` in `provider_service.py` |
| **Admin API `category_ids` support** | ❌ **MISSING** | `ProviderUpdateInput` has only plain-text `category` field |
| **Admin PATCH → ServiceExpertise sync** | ❌ **MISSING** | PATCH endpoint does NOT call `_sync_service_expertises()` |
| **Vehicle Type Compatibility field** | ❌ **MISSING** | No `supported_vehicle_types` on any provider model |
| **Vehicle Type→Provider junction table** | ❌ **MISSING** | No dedicated relationship table exists |
---
## 1⃣ CATEGORY TAXONOMY (ExpertiseTag) — ✅ FULLY IMPLEMENTED
### Model: `ExpertiseTag` (`backend/app/models/marketplace/service.py:83`)
- **Table:** `marketplace.expertise_tags`
- **Multi-language:** `name_hu`, `name_en`, `name_translations` (JSONB)
- **4-level hierarchy:** `parent_id`, `level` (0-3), `path`
- **Self-referential tree:** `children` / `parent` relationship
### Junction: `ServiceExpertise` (`backend/app/models/marketplace/service.py:146`)
- **Table:** `marketplace.service_expertises`
- **Links:** `service_id``ServiceProfile.id` ←→ `expertise_id``ExpertiseTag.id`
- **Extra:** `confidence_level`
### Vehicle Types in the 4-Level Taxonomy (Level 0)
From `seed_expertise_taxonomy.py`:
| Key | HU Name | EN Name |
|-----|---------|---------|
| `szemelygepjarmu` | Személygépjármű | Passenger Car |
| `motor` | Motorkerékpár | Motorcycle |
| `kishaszonjarmu` | Kishaszonjármű | LCV |
| `nagyhaszonjarmu_kamion` | Nagyhaszonjármű (Kamion) | HGV / Truck |
| `munkagep_traktor` | Munkagép / Traktor | Agricultural / Construction Machinery |
| `potkocsi_utanfuto` | Pótkocsi / Utánfutó | Trailer |
Plus **Universal** Level-1: Fuel Stations, Restaurants, Accommodation, Parking.
---
## 2⃣ VEHICLE TYPE COMPATIBILITY — ❌ COMPLETELY MISSING
### `ServiceProvider` (`backend/app/models/identity/social.py:23`)
- Fields: `id`, `name`, `address`, `category` (str), `city`, contact fields, status, source
- **NO** `supported_vehicle_types`
- **NO** vehicle type relationship
### `ServiceProfile` (`backend/app/models/marketplace/service.py:21`)
- Has: `expertises` (ServiceExpertise relationship), `specialization_tags` (JSONB)
- **NO** `supported_vehicle_types`
### Implicit tracking only
The ONLY way vehicle type compatibility is tracked today is through the 4-level ExpertiseTag hierarchy (Level-0 tags). This is implicit, not explicit — no dedicated queryable field exists.
### `vehicle.vehicle_types` table exists but UNLINKED
`VehicleType` (`backend/app/models/vehicle/vehicle_definitions.py:17`) is in the `vehicle` schema, used for catalog data. No FK link to providers.
---
## 3⃣ ADMIN PROVIDER SCHEMAS — ⚠️ PARTIALLY IMPLEMENTED
### Public Schemas (✅ Handle `category_ids`)
| Schema | `category_ids` | `new_tags` |
|--------|---------------|-----------|
| `ProviderQuickAddIn` (`schemas/provider.py:135`) | ✅ `Optional[List[int]]` | ✅ `Optional[List[str]]` |
| `ProviderUpdateIn` (`schemas/provider.py:173`) | ✅ `Optional[List[int]]` | ✅ `Optional[List[str]]` |
### Admin Schema (❌ MISSING)
| Schema | `category_ids` | `new_tags` | `supported_vehicle_types` |
|--------|---------------|-----------|--------------------------|
| `ProviderUpdateInput` (`admin_providers.py:123`) | ❌ **MISSING** | ❌ **MISSING** | ❌ **MISSING** |
Admin `ProviderUpdateInput` only has: `name`, `city`, `address_zip`, `address_street_name`, `address_street_type`, `address_house_number`, `plus_code`, `contact_phone`, `contact_email`, `website`, plain-text `category`, `status`, `validation_score`.
### Admin PATCH (`admin_providers.py:1072`) — No ServiceExpertise sync
The `update_provider()` function sets fields via `setattr` and logs to AuditLog, but **never** calls `_sync_service_expertises()`.
Compare with public endpoint in `provider_service.py:1070` which:
1. Collects `category_ids` + `new_tag_ids`
2. Creates new `ExpertiseTag` for `new_tags`
3. Calls `_sync_service_expertises()` at line 1094
---
## 4⃣ REQUIRED BACKEND CHANGES BEFORE FRONTEND MATRIX UI
### 🔴 P0 — MUST FIX: Admin `ProviderUpdateInput` schema
**File:** `backend/app/api/v1/endpoints/admin_providers.py:123`
Add: `category_ids: Optional[List[int]]`, `new_tags: Optional[List[str]]`
### 🔴 P0 — MUST FIX: Admin PATCH endpoint
**File:** `backend/app/api/v1/endpoints/admin_providers.py:1072`
Add `ServiceExpertise` sync after field updates.
### 🔴 P0 — MUST CREATE: Vehicle Type Compatibility Model
New junction table `marketplace.provider_vehicle_types` linking `service_provider_id` to `expertise_tags.id` (Level-0 vehicle type tags).
### 🟡 P1 — ADD: `supported_vehicle_type_ids` to ProviderUpdateInput
List of Level-0 ExpertiseTag IDs.
### 🟡 P1 — ADD: Categories + Vehicle Types to ProviderDetail response
Add `supported_vehicle_types: List[dict]` and `categories: List[dict]` fields.
### 🟢 P2 — ADD: Frontend Edit Page `/providers/[id]/edit`
Tabbed matrix layout: Basic Info | Categories (4-level tree) | Vehicle Types | Location
---
## 5⃣ DATA FLOW COMPARISON
```
CURRENT STATE:
ServiceProvider ──category(str)──→ [plain text, no relationship]
└──ServiceProfile──ServiceExpertise──ExpertiseTag (public API only)
TARGET STATE:
ServiceProvider ──category_ids──→ ServiceExpertise──ExpertiseTag (service categories)
└──supported_vehicle_type_ids──→ ProviderVehicleType──ExpertiseTag (Level-0 types)
```
---
## 6⃣ KEY TAKEAWAYS
1. **ExpertiseTag hierarchy is production-ready** — 6 vehicle types at Level-0, full multi-language.
2. **Public API already handles `category_ids`** — admin PATCH is the gap.
3. **Vehicle type compatibility is entirely new** — reuse Level-0 ExpertiseTags as vehicle type references.
4. **`category` varchar field is deprecated** — admin PATCH needs to catch up to structured ServiceExpertise.

View File

@@ -0,0 +1,336 @@
# 🔍 P0 RECONNAISSANCE — Service Provider & Gamification State Audit Report
**Date:** 2026-06-30
**Auditor:** Rendszer-Architect (DeepSeek Reasoner)
**Mode:** READ-ONLY — NO FILES MODIFIED
**Status:** COMPLETE
---
## Executive Summary
This report documents the deep-dive reconnaissance audit of the Service Provider ecosystem, Ratings/Validations, and Gamification modules. The audit covered 3 database schemas (`marketplace`, `identity`, `gamification`), 6 model files, 4 service files, 5 API endpoint files, and 12 router registrations.
**CRITICAL GAP FOUND:** No `admin_providers.py` router exists — the admin provider approval workflow is completely missing from the API layer. Admin UI for `/providers` pages cannot be built without this foundational layer.
---
## 1. Database Model Map
### 1.1 Marketplace Schema (`marketplace`)
| Table | File | Purpose |
|-------|------|---------|
| `service_profiles` | `backend/app/models/marketplace/service.py:21` | Rich service profile linked to orgs or providers |
| `expertise_tags` | `backend/app/models/marketplace/service.py:97` | 4-level category hierarchy (L0-L3) |
| `service_expertises` | `backend/app/models/marketplace/service.py:147` | Junction table: ServiceProfile ↔ ExpertiseTag |
| `service_staging` | `backend/app/models/marketplace/service.py:159` | Robot-collected service data (pre-moderation) |
| `service_staging` | `backend/app/models/marketplace/staged_data.py:1` | Extended staging model (extend_existing=True) |
| `discovery_parameters` | `backend/app/models/marketplace/service.py:191` | Robot discovery parameters |
| `cost` | `backend/app/models/marketplace/service.py:205` | Service cost structure |
### 1.2 Identity Schema (`identity` — Social)
| Table | File | Purpose |
|-------|------|---------|
| `service_providers` | `backend/app/models/identity/social.py:133` | Crowdsourced provider submissions (P0 refactor) |
| `votes` | `backend/app/models/identity/social.py:186` | Community validation (+1/-1 per user/provider) |
| `competitions` | `backend/app/models/identity/social.py:209` | Social competitions |
| `user_scores` | `backend/app/models/identity/social.py:243` | User competition scores |
| `service_reviews` | `backend/app/models/identity/social.py:266` | Verified reviews tied to financial transactions |
### 1.3 Gamification Schema (`gamification`)
| Table | File | Purpose |
|-------|------|---------|
| `point_rules` | `backend/app/models/gamification/gamification.py:31` | Dynamic point rules (action_key → points) |
| `points_ledger` | `backend/app/models/gamification/gamification.py:89` | Point transaction log |
| `user_stats` | `backend/app/models/gamification/gamification.py:103` | User gamification stats |
| `user_contributions` | `backend/app/models/gamification/gamification.py:137` | Contribution tracking + moderation |
| `level_configs` | `backend/app/models/gamification/gamification.py:179` | Level definitions (positive + penalty) |
| `seasonal_competitions` | `backend/app/models/gamification/gamification.py:213` | Seasonal events |
| `badges` | `backend/app/models/gamification/gamification.py:252` | Achievement badges |
| `user_badges` | `backend/app/models/gamification/gamification.py:275` | User-badge assignments |
| `seasons` | `backend/app/models/gamification/gamification.py:296` | Competition seasons |
---
## 2. Trust & Validation Score Architecture
### 2.1 Score Fields on Models
| Field | Model | Default | Purpose |
|-------|-------|---------|---------|
| `validation_score` | `ServiceProvider` | `0` | Community validation aggregate (+1/-1 from Votes) |
| `trust_score` | `ServiceProfile` | `30` | Trust engine score (Gondos Gazda Index) |
| `is_verified` | `ServiceProfile` | `False` | Admin/manual verification flag |
| `rating_*_avg` | `ServiceProfile` | `NULL` | Aggregated verified review ratings (4 dimensions) |
| `rating_overall` | `ServiceProfile` | `NULL` | Overall rating from verified reviews |
### 2.2 CRITICAL: `provider_validations` Table NEVER Created
The Masterbook 2.0 spec mentioned a `provider_validations` table for XP farming prevention, **but it was never created in the codebase**. No model file, no Alembic migration, no sync_engine artifact exists for this table.
**Impact:** XP farming prevention for provider submissions relies solely on:
- `UserContribution` status (pending/approved/rejected) — exists
- `UserStats.providers_added_count` — exists
- `Vote` table duplicate prevention (UniqueConstraint on user_id + provider_id) — exists
The dedicated anti-farming table was spec-only and never implemented.
### 2.3 Review Validation Rules
`ServiceReview` (from `backend/app/models/identity/social.py:266`):
- Mandatory FK to `audit.financial_ledger.transaction_id` (UniqueConstraint)
- 4 rating dimensions: `price_rating`, `quality_rating`, `time_rating`, `communication_rating` (1-10 scale)
- Only users with verified financial transaction can review
- `ServiceProfile.rating_*_avg` fields aggregate these values
---
## 3. Gamification Engine Analysis
### 3.1 Dynamic Point Rules
The gamification engine uses **dynamic rules** from `gamification.point_rules` table (not hardcoded):
| Action Key | Points | Description |
|------------|--------|-------------|
| `ADD_NEW_PROVIDER` | 500 | Adding a new service provider |
| `USE_UNVERIFIED_PROVIDER` | 200 | Using an unverified provider |
| `RATE_PROVIDER` | 250 | Rating a provider |
| `UPDATE_PROVIDER` | 100 | Updating provider info |
Seeded by `backend/app/scripts/seed_gamification_rules.py`.
### 3.2 GamificationService Flow
From `backend/app/services/gamification_service.py`:
1. `process_activity(user_id, action_key, ...)` — Main entry point
2. Reads `PointRule` from DB by action_key
3. Applies level multiplier via `_calculate_multiplier()`
4. Penalty system: 4 levels (L0=100%, L1=50%, L2=10%, L3=0%)
5. Calls `_handle_level_up()` for cross-level logic
6. Social points → credits conversion at configurable rate
7. Config loaded from `GAMIFICATION_MASTER_CONFIG` system parameter
### 3.3 Provider Service Gamification Integration
From `backend/app/services/provider_service.py`:
- `_award_provider_points()` — Reads PointRule from DB, awards through GamificationService
- Called on: `quick_add_provider()` and `update_provider()`
- `UserContribution` records created with status "pending" for admin review
---
## 4. API Endpoint Inventory
### 4.1 User-Facing Provider Endpoints (`/providers` prefix)
From `backend/app/api/v1/endpoints/providers.py`:
| Method | Path | Description |
|--------|------|-------------|
| GET | `/providers/categories/tree` | Full hierarchical category tree (4-level) |
| GET | `/providers/categories/autocomplete` | Text search for L2-L3 tags |
| GET | `/providers/categories` | Flat list of expertise categories |
| GET | `/providers/search` | Unified provider search (3 sources) |
| POST | `/providers/quick-add` | Quick provider submission (crowdsourced) |
| PUT | `/providers/{provider_id}` | Update provider details (hybrid save) |
### 4.2 Admin Gamification Endpoints (`/admin/gamification` prefix)
From `backend/app/api/v1/endpoints/admin_gamification.py`:
| Method | Path | Description |
|--------|------|-------------|
| GET/POST | `/admin/gamification/point-rules` | List/Create point rules |
| GET/PUT/DELETE | `/admin/gamification/point-rules/{id}` | CRUD single rule |
| GET/POST | `/admin/gamification/levels` | List/Create level configs |
| PUT | `/admin/gamification/levels/{id}` | Update level config |
| GET/POST | `/admin/gamification/badges` | List/Create badges |
| GET/PUT | `/admin/gamification/user-stats` | List/Update user stats |
| GET | `/admin/gamification/user-stats/{id}` | Get user stat detail |
| POST | `/admin/gamification/user-stats/{id}/penalty` | Apply penalty |
| POST | `/admin/gamification/user-stats/{id}/reward` | Apply reward |
| GET | `/admin/gamification/contributions` | List contributions |
| PUT | `/admin/gamification/contributions/{id}/review` | Approve/reject contribution |
| GET/POST | `/admin/gamification/seasons` | List/Create seasons |
| PUT/DELETE | `/admin/gamification/seasons/{id}` | Update/Delete season |
| GET/POST | `/admin/gamification/competitions` | List/Create competitions |
| PUT/DELETE | `/admin/gamification/competitions/{id}` | Update/Delete competition |
| GET | `/admin/gamification/leaderboard` | Admin leaderboard |
| GET | `/admin/gamification/dashboard` | Dashboard summary |
| GET/PUT | `/admin/gamification/master-config` | Master config CRUD |
### 4.3 User-Facing Gamification Endpoints (`/gamification` prefix)
From `backend/app/api/v1/endpoints/gamification.py`:
| Method | Path | Description |
|--------|------|-------------|
| GET | `/gamification/my-stats` | Current user stats |
| GET | `/gamification/leaderboard` | Public leaderboard |
| GET | `/gamification/seasons` | List seasons |
| GET | `/gamification/my-contributions` | User contributions |
| POST | `/gamification/submit-service` | Submit service (gamified) |
| GET | `/gamification/quiz/daily` | Daily quiz |
| POST | `/gamification/quiz/answer` | Answer quiz |
| GET/POST | `/gamification/badges` | Badge listing/awarding |
| GET | `/gamification/achievements` | Achievement list |
### 4.4 Existing Admin Routers
| Router File | Prefix | Module |
|-------------|--------|--------|
| `admin.py` | `/admin` | General admin (health, params, pending actions) |
| `admin_gamification.py` | `/admin/gamification` | Gamification admin CRUD |
| `admin_organizations.py` | `/admin/organizations` | Organization CRM |
| `admin_users.py` | `/admin/users` | User management |
| `admin_persons.py` | `/admin/persons` | Person management |
| `admin_packages.py` | `/admin/packages` | Package management |
| `admin_services.py` | `/admin/services` | Service catalog |
| `admin_permissions.py` | `(root)` | Permission/RBAC management |
---
## 5. GAP ANALYSIS — Missing Admin Provider API
### 5.1 CRITICAL GAP: No `admin_providers.py` Router
**There is NO `/admin/providers` router.** The file `admin_providers.py` does not exist in the endpoints directory. The following router files DO exist:
- `admin_gamification.py`, `admin_organizations.py`, `admin_users.py`, `admin_persons.py`, `admin_packages.py`, `admin_services.py`, `admin_permissions.py`
- `admin.py` (general admin)
**Impact:** Admin users cannot manage providers through the API. The Nuxt Admin Frontend cannot build `/providers` pages without this backend layer.
### 5.2 Complete List of Missing Endpoints
| # | Method | Path | Purpose |
|---|--------|------|---------|
| 1 | GET | `/admin/providers` | List all providers from 3 sources (orgs, staging, crowd) |
| 2 | GET | `/admin/providers/pending` | List pending providers awaiting approval |
| 3 | GET | `/admin/providers/{id}` | Get detailed provider info (merged from all sources) |
| 4 | PUT | `/admin/providers/{id}/approve` | Approve provider → set status, award submitter XP |
| 5 | PUT | `/admin/providers/{id}/reject` | Reject provider with reason → flag UserContribution |
| 6 | GET | `/admin/providers/staging` | List robot-collected staging entries |
| 7 | PUT | `/admin/providers/staging/{id}/promote` | Promote staging → ServiceProfile (verify & create org) |
| 8 | PUT | `/admin/providers/staging/{id}/reject` | Reject staging entry with reason |
| 9 | GET | `/admin/providers/{id}/votes` | List community validation votes for a provider |
| 10 | GET | `/admin/providers/{id}/reviews` | List service reviews for a provider |
| 11 | PUT | `/admin/providers/{provider_id}/verify` | Toggle verified status on ServiceProfile |
| 12 | PUT | `/admin/providers/{provider_id}/trust-score` | Manually adjust trust_score |
| 13 | GET | `/admin/providers/validations` | List pending community validations (Votes table) |
| 14 | PUT | `/admin/providers/staging/{id}` | Update staging entry (edit robot-collected data) |
### 5.3 Dependencies for Building These Endpoints
| Dependency | Current Status | Notes |
|------------|---------------|-------|
| `ServiceProvider` model (`social.py:133`) | Complete | Includes validation_score, status, source |
| `ServiceProfile` model (`service.py:21`) | Complete | Includes trust_score, is_verified, verification_log |
| `ServiceStaging` model (`service.py:159`) | Complete | Includes status, validation_level, audit_trail |
| `Vote` model (`social.py:186`) | Complete | Includes weight, user_id, provider_id |
| `ServiceReview` model (`social.py:266`) | Complete | Verified reviews with financial transaction linkage |
| `GamificationService` (`gamification_service.py`) | Complete | Can award XP when admin approves |
| `UserContribution` model (`gamification.py:137`) | Complete | Status field for pending/approved/rejected |
| Permission `providers:manage` | Unknown | Needs to exist in RBAC or be created |
| Schemas for admin provider endpoints | MISSING | Need `ProviderAdminOut`, `ProviderAdminList`, etc. |
### 5.4 Provider Data Flow (Current State)
```
+-------------------+ +-------------------+ +-------------------+
| Robot (Staging) | | Crowdsourced | | Verified Org |
| service_staging | | service_providers | | fleet.orgs |
| status: pending | | status: pending | | is_verified: T |
+--------+----------+ +--------+----------+ +--------+----------+
| | |
v v v
+---------------------------------------------------------------+
| User-Facing API (providers.py) |
| GET /providers/search -- UNION of all 3 sources |
| POST /providers/quick-add -- Creates ServiceProvider |
| PUT /providers/{id} -- Update with migration logic |
+---------------------------------------------------------------+
| | |
| ** NO ADMIN LAYER EXISTS ** |
v v v
+---------------------------------------------------------------+
| MISSING: Admin Provider Workflow |
| - No approval/rejection for pending providers |
| - No staging promotion to verified org |
| - No trust_score manual adjustment |
| - No community validation vote review |
| - No service review moderation |
+---------------------------------------------------------------+
```
---
## 6. Gitea Task Breakdown (Required for Implementation)
Based on the 3A granularity principle, the implementation of the admin provider layer requires these Gitea tasks:
### Epic: Admin Provider Management
| # | Task | Priority | Dependencies |
|---|------|----------|-------------|
| 1 | Create schemas for admin provider endpoints | P0 | None |
| 2 | Implement `admin_providers.py` - List/Get providers | P0 | Schema |
| 3 | Implement approve/reject provider logic | P0 | GamificationService |
| 4 | Implement staging management (list/promote/reject) | P0 | None |
| 5 | Implement community validation vote review | P1 | #4 |
| 6 | Implement service review moderation | P1 | #5 |
| 7 | Register router in `api.py` at `/admin/providers` | P0 | #2 |
| 8 | Implement manual trust_score adjustment endpoint | P1 | #6 |
| 9 | Create RBAC permission `providers:manage` | P0 | None |
| 10 | E2E tests for admin provider workflow | P1 | #2-#9 |
---
## 7. Gamification Gaps & Observations
### 7.1 No Admin-Triggered Gamification Rewards
When an admin approves a provider submission, there is **no endpoint** to trigger `GamificationService.process_activity()` for the submitter. The `UserContribution` record exists but there's no automated XP award upon approval.
**Fix:** The `PUT /admin/providers/{id}/approve` endpoint must call:
```python
await gamification_service.process_activity(
db=db,
user_id=submitter_user_id,
action_key="ADD_NEW_PROVIDER",
source_type="provider_approval",
source_id=provider_id
)
```
### 7.2 No Rejected Contribution Feedback Loop
When a provider is rejected, the `UserContribution.status` should be set to `rejected` with a rejection reason, but there's no endpoint to trigger this for admin workflows.
### 7.3 Gamification Config is Dynamic but Has No Validation
The `GAMIFICATION_MASTER_CONFIG` system parameter can be edited freely via the admin gamification API, but there's **no schema validation** on the JSONB structure. A malformed config could break the gamification engine.
---
## 8. Summary of Findings
| Category | Status | Details |
|----------|--------|---------|
| Database Models | COMPLETE | All 15+ models across 3 schemas |
| User-Facing Provider API | COMPLETE | 6 endpoints in providers.py |
| User-Facing Gamification API | COMPLETE | 15+ endpoints in gamification.py |
| Admin Gamification API | COMPLETE | 25+ endpoints in admin_gamification.py |
| Gamification Service | COMPLETE | Full engine with dynamic rules, penalties |
| Provider Service | COMPLETE | P0 hybrid vendor refactor applied |
| **Admin Provider API** | **MISSING** | **No admin_providers.py - 14 endpoints missing** |
| **provider_validations table** | **NEVER CREATED** | Spec-only, not in codebase |
| **Trust Engine** | **STUB** | trust_score exists but no active scoring logic |
| **Admin Rejection Gamification** | **MISSING** | No endpoint to award/reject XP on moderation |
---
*End of P0 Reconnaissance Report. This is a READ-ONLY analysis - no files were modified.*

View File

@@ -14,6 +14,8 @@ type HydrationStrategies = {
type LazyComponent<T> = DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T type LazyComponent<T> = DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T
export const LanguageSwitcher: typeof import("../components/LanguageSwitcher.vue")['default']
export const TreeNode: typeof import("../components/TreeNode.vue")['default']
export const ChartsUserTrendChart: typeof import("../components/charts/UserTrendChart.vue")['default'] export const ChartsUserTrendChart: typeof import("../components/charts/UserTrendChart.vue")['default']
export const GaragesGarageFleetTab: typeof import("../components/garages/GarageFleetTab.vue")['default'] export const GaragesGarageFleetTab: typeof import("../components/garages/GarageFleetTab.vue")['default']
export const GaragesGarageGeneralTab: typeof import("../components/garages/GarageGeneralTab.vue")['default'] export const GaragesGarageGeneralTab: typeof import("../components/garages/GarageGeneralTab.vue")['default']
@@ -43,6 +45,8 @@ export const Head: typeof import("../node_modules/nuxt/dist/head/runtime/compone
export const Html: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Html'] export const Html: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Html']
export const Body: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Body'] export const Body: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Body']
export const NuxtIsland: typeof import("../node_modules/nuxt/dist/app/components/nuxt-island")['default'] export const NuxtIsland: typeof import("../node_modules/nuxt/dist/app/components/nuxt-island")['default']
export const LazyLanguageSwitcher: LazyComponent<typeof import("../components/LanguageSwitcher.vue")['default']>
export const LazyTreeNode: LazyComponent<typeof import("../components/TreeNode.vue")['default']>
export const LazyChartsUserTrendChart: LazyComponent<typeof import("../components/charts/UserTrendChart.vue")['default']> export const LazyChartsUserTrendChart: LazyComponent<typeof import("../components/charts/UserTrendChart.vue")['default']>
export const LazyGaragesGarageFleetTab: LazyComponent<typeof import("../components/garages/GarageFleetTab.vue")['default']> export const LazyGaragesGarageFleetTab: LazyComponent<typeof import("../components/garages/GarageFleetTab.vue")['default']>
export const LazyGaragesGarageGeneralTab: LazyComponent<typeof import("../components/garages/GarageGeneralTab.vue")['default']> export const LazyGaragesGarageGeneralTab: LazyComponent<typeof import("../components/garages/GarageGeneralTab.vue")['default']>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{_ as a}from"./15rOo9Uq.js";import{_ as i}from"./DlAUqK2U.js";import{u,o as c,c as l,a as e,t as r,b as d,w as p,d as f}from"./BMplCfh_.js";const m={class:"antialiased bg-white dark:bg-black dark:text-white font-sans grid min-h-screen overflow-hidden place-content-center text-black"},g={class:"max-w-520px text-center z-20"},b=["textContent"],h=["textContent"],x={class:"flex items-center justify-center w-full"},y={__name:"error-404",props:{appName:{type:String,default:"Nuxt"},version:{type:String,default:""},status:{type:Number,default:404},statusText:{type:String,default:"Not Found"},description:{type:String,default:"Sorry, the page you are looking for could not be found."},backHome:{type:String,default:"Go back home"}},setup(t){const n=t;return u({title:`${n.status} - ${n.statusText} | ${n.appName}`,script:[{innerHTML:`!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)}).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`}],style:[{innerHTML:'*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}h1,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(k,o)=>{const s=a;return c(),l("div",m,[o[0]||(o[0]=e("div",{class:"fixed left-0 right-0 spotlight z-10"},null,-1)),e("div",g,[e("h1",{class:"font-medium mb-8 sm:text-10xl text-8xl",textContent:r(t.status)},null,8,b),e("p",{class:"font-light leading-tight mb-16 px-8 sm:px-0 sm:text-4xl text-xl",textContent:r(t.description)},null,8,h),e("div",x,[d(s,{to:"/",class:"cursor-pointer gradient-border px-4 py-2 sm:px-6 sm:py-3 sm:text-xl text-md"},{default:p(()=>[f(r(t.backHome),1)]),_:1})])])])}}},z=i(y,[["__scopeId","data-v-1bd9e11a"]]);export{z as default}; import{_ as a}from"./CuTzR9Y8.js";import{_ as i}from"./DlAUqK2U.js";import{u,o as c,c as l,a as e,t as r,b as d,w as p,d as f}from"./Duy9QquX.js";const m={class:"antialiased bg-white dark:bg-black dark:text-white font-sans grid min-h-screen overflow-hidden place-content-center text-black"},g={class:"max-w-520px text-center z-20"},b=["textContent"],h=["textContent"],x={class:"flex items-center justify-center w-full"},y={__name:"error-404",props:{appName:{type:String,default:"Nuxt"},version:{type:String,default:""},status:{type:Number,default:404},statusText:{type:String,default:"Not Found"},description:{type:String,default:"Sorry, the page you are looking for could not be found."},backHome:{type:String,default:"Go back home"}},setup(t){const n=t;return u({title:`${n.status} - ${n.statusText} | ${n.appName}`,script:[{innerHTML:`!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)}).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`}],style:[{innerHTML:'*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}h1,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(k,o)=>{const s=a;return c(),l("div",m,[o[0]||(o[0]=e("div",{class:"fixed left-0 right-0 spotlight z-10"},null,-1)),e("div",g,[e("h1",{class:"font-medium mb-8 sm:text-10xl text-8xl",textContent:r(t.status)},null,8,b),e("p",{class:"font-light leading-tight mb-16 px-8 sm:px-0 sm:text-4xl text-xl",textContent:r(t.description)},null,8,h),e("div",x,[d(s,{to:"/",class:"cursor-pointer gradient-border px-4 py-2 sm:px-6 sm:py-3 sm:text-xl text-md"},{default:p(()=>[f(r(t.backHome),1)]),_:1})])])])}}},z=i(y,[["__scopeId","data-v-1bd9e11a"]]);export{z as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
const t={common:{loading:{t:0,b:{t:2,i:[{t:3}],s:"Načítavanie..."}},saving:{t:0,b:{t:2,i:[{t:3}],s:"Ukladanie..."}},error:{t:0,b:{t:2,i:[{t:3}],s:"Došlo k chybe"}},retry:{t:0,b:{t:2,i:[{t:3}],s:"Opakovať"}},save:{t:0,b:{t:2,i:[{t:3}],s:"Uložiť"}},saving_data:{t:0,b:{t:2,i:[{t:3}],s:"Ukladanie údajov..."}},cancel:{t:0,b:{t:2,i:[{t:3}],s:"Zrušiť"}},discard:{t:0,b:{t:2,i:[{t:3}],s:"Zahodiť"}},delete:{t:0,b:{t:2,i:[{t:3}],s:"Zmazať"}},delete_confirm:{t:0,b:{t:2,i:[{t:3}],s:"Naozaj chcete zmazať túto položku?"}},delete_btn:{t:0,b:{t:2,i:[{t:3}],s:"Zmazať"}},delete_title:{t:0,b:{t:2,i:[{t:3}],s:"Potvrdiť Zmazanie"}},deleted:{t:0,b:{t:2,i:[{t:3}],s:"Úspešne zmazané"}},edit:{t:0,b:{t:2,i:[{t:3}],s:"Upraviť"}},edit_title:{t:0,b:{t:2,i:[{t:3}],s:"Upraviť Položku"}},create:{t:0,b:{t:2,i:[{t:3}],s:"Vytvoriť"}},create_btn:{t:0,b:{t:2,i:[{t:3}],s:"Vytvoriť"}},create_title:{t:0,b:{t:2,i:[{t:3}],s:"Vytvoriť Nové"}},create_first:{t:0,b:{t:2,i:[{t:3}],s:"Vytvorte svoju prvú položku pre začiatok"}},search:{t:0,b:{t:2,i:[{t:3}],s:"Hľadať"}},search_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Hľadať..."}},filter:{t:0,b:{t:2,i:[{t:3}],s:"Filter"}},no_results:{t:0,b:{t:2,i:[{t:3}],s:"Nenašli sa žiadne výsledky"}},no_results_hint:{t:0,b:{t:2,i:[{t:3}],s:"Skúste upraviť kritériá vyhľadávania alebo filtra"}},no_items:{t:0,b:{t:2,i:[{t:3}],s:"Žiadne položky k dispozícii"}},actions:{t:0,b:{t:2,i:[{t:3}],s:"Akcie"}},status:{t:0,b:{t:2,i:[{t:3}],s:"Stav"}},active:{t:0,b:{t:2,i:[{t:3}],s:"Aktívny"}},inactive:{t:0,b:{t:2,i:[{t:3}],s:"Neaktívny"}},activate:{t:0,b:{t:2,i:[{t:3}],s:"Aktivovať"}},deactivate:{t:0,b:{t:2,i:[{t:3}],s:"Deaktivovať"}},name:{t:0,b:{t:2,i:[{t:3}],s:"Názov"}},name_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Zadajte názov"}},description:{t:0,b:{t:2,i:[{t:3}],s:"Popis"}},desc_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Zadajte popis"}},details:{t:0,b:{t:2,i:[{t:3}],s:"Detaily"}},email:{t:0,b:{t:2,i:[{t:3}],s:"Email"}},id:{t:0,b:{t:2,i:[{t:3}],s:"ID"}},type:{t:0,b:{t:2,i:[{t:3}],s:"Typ"}},type_corporate:{t:0,b:{t:2,i:[{t:3}],s:"Firemný"}},view:{t:0,b:{t:2,i:[{t:3}],s:"Zobraziť"}},updated:{t:0,b:{t:2,i:[{t:3}],s:"Úspešne aktualizované"}},save_error:{t:0,b:{t:2,i:[{t:3}],s:"Uloženie zlyhalo"}},invalid_json:{t:0,b:{t:2,i:[{t:3}],s:"Neplatný formát JSON"}},prev:{t:0,b:{t:2,i:[{t:3}],s:"Predchádzajúci"}},next:{t:0,b:{t:2,i:[{t:3}],s:"Ďalší"}},none:{t:0,b:{t:2,i:[{t:3}],s:"Žiadne"}},reset:{t:0,b:{t:2,i:[{t:3}],s:"Reset"}},confirm:{t:0,b:{t:2,i:[{t:3}],s:"Potvrdiť"}},close:{t:0,b:{t:2,i:[{t:3}],s:"Zavrieť"}},back:{t:0,b:{t:2,i:[{t:3}],s:"Späť"}},continue:{t:0,b:{t:2,i:[{t:3}],s:"Pokračovať"}},exit:{t:0,b:{t:2,i:[{t:3}],s:"Ukončiť"}},all:{t:0,b:{t:2,i:[{t:3}],s:"Všetky"}},select_all:{t:0,b:{t:2,i:[{t:3}],s:"Vybrať Všetky"}},deselect_all:{t:0,b:{t:2,i:[{t:3}],s:"Odznačiť Všetky"}},expires:{t:0,b:{t:2,i:[{t:3}],s:"Vyprší"}},created:{t:0,b:{t:2,i:[{t:3}],s:"Vytvorené"}},start_date:{t:0,b:{t:2,i:[{t:3}],s:"Dátum Začiatku"}},end_date:{t:0,b:{t:2,i:[{t:3}],s:"Dátum Ukončenia"}},points:{t:0,b:{t:2,i:[{t:3}],s:"Body"}},level:{t:0,b:{t:2,i:[{t:3}],s:"Úroveň"}},user:{t:0,b:{t:2,i:[{t:3}],s:"Používateľ"}},user_id:{t:0,b:{t:2,i:[{t:3}],s:"ID Používateľa"}},services:{t:0,b:{t:2,i:[{t:3}],s:"Služby"}},discoveries:{t:0,b:{t:2,i:[{t:3}],s:"Objavy"}},restriction:{t:0,b:{t:2,i:[{t:3}],s:"Obmedzenie"}},restriction_mild:{t:0,b:{t:2,i:[{t:3}],s:"Mierne"}},restriction_moderate:{t:0,b:{t:2,i:[{t:3}],s:"Stredné"}},restriction_severe:{t:0,b:{t:2,i:[{t:3}],s:"Prísne"}},min_xp:{t:0,b:{t:2,i:[{t:3}],s:"Min XP"}},xp:{t:0,b:{t:2,i:[{t:3}],s:"XP"}},penalty:{t:0,b:{t:2,i:[{t:3}],s:"Trest"}},feature_flags:{t:0,b:{t:2,i:[{t:3}],s:"Funkcie"}},permission:{t:0,b:{t:2,i:[{t:3}],s:"Oprávnenie"}},role:{t:0,b:{t:2,i:[{t:3}],s:"Rola"}},domain:{t:0,b:{t:2,i:[{t:3}],s:"Doména"}},action:{t:0,b:{t:2,i:[{t:3}],s:"Akcia"}},value:{t:0,b:{t:2,i:[{t:3}],s:"Hodnota"}},key:{t:0,b:{t:2,i:[{t:3}],s:"Kľúč"}},enabled:{t:0,b:{t:2,i:[{t:3}],s:"Povolené"}},disabled:{t:0,b:{t:2,i:[{t:3}],s:"Zakázané"}},yes:{t:0,b:{t:2,i:[{t:3}],s:"Áno"}},no:{t:0,b:{t:2,i:[{t:3}],s:"Nie"}},on:{t:0,b:{t:2,i:[{t:3}],s:"Zapnuté"}},off:{t:0,b:{t:2,i:[{t:3}],s:"Vypnuté"}}}};export{t as default};

View File

@@ -1 +1 @@
import{ac as _,l as u,m as a}from"./BMplCfh_.js";const h=_("region",()=>{const t=u([]),n=u(null),c=u(!1),l=u(null),o=a(()=>t.value.find(e=>e.country_code==="DEFAULT")||null),s=a(()=>{const e=new Map;for(const r of t.value)e.set(r.country_code,r);return e}),i=a(()=>n.value?.currency||o.value?.currency||"EUR"),v=a(()=>n.value?.locale_code||o.value?.locale_code||"en-EU"),f=a(()=>n.value?.default_vat_rate??o.value?.default_vat_rate??0),d=a(()=>n.value?.timezone||o.value?.timezone||"Europe/Brussels");async function g(){if(!(t.value.length>0)){c.value=!0,l.value=null;try{const e=await $fetch("/api/v1/system/regions");t.value=e||[]}catch(e){l.value=e?.data?.detail||e?.message||"Failed to load regions",console.error("[RegionStore] Fetch error:",e)}finally{c.value=!1}}}function m(e){const r=t.value.find(R=>R.country_code===e);n.value=r||o.value}function y(e){return s.value.get(e)}return{regions:t,activeRegion:n,isLoading:c,error:l,defaultRegion:o,regionMap:s,activeCurrency:i,activeLocale:v,activeVatRate:f,activeTimezone:d,fetchRegions:g,setActiveRegion:m,getRegion:y}});export{h as u}; import{ac as _,l as u,m as a}from"./Duy9QquX.js";const h=_("region",()=>{const t=u([]),n=u(null),c=u(!1),l=u(null),o=a(()=>t.value.find(e=>e.country_code==="DEFAULT")||null),s=a(()=>{const e=new Map;for(const r of t.value)e.set(r.country_code,r);return e}),i=a(()=>n.value?.currency||o.value?.currency||"EUR"),v=a(()=>n.value?.locale_code||o.value?.locale_code||"en-EU"),f=a(()=>n.value?.default_vat_rate??o.value?.default_vat_rate??0),d=a(()=>n.value?.timezone||o.value?.timezone||"Europe/Brussels");async function g(){if(!(t.value.length>0)){c.value=!0,l.value=null;try{const e=await $fetch("/api/v1/system/regions");t.value=e||[]}catch(e){l.value=e?.data?.detail||e?.message||"Failed to load regions",console.error("[RegionStore] Fetch error:",e)}finally{c.value=!1}}}function m(e){const r=t.value.find(R=>R.country_code===e);n.value=r||o.value}function y(e){return s.value.get(e)}return{regions:t,activeRegion:n,isLoading:c,error:l,defaultRegion:o,regionMap:s,activeCurrency:i,activeLocale:v,activeVatRate:f,activeTimezone:d,fetchRegions:g,setActiveRegion:m,getRegion:y}});export{h as u};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
const t={common:{loading:{t:0,b:{t:2,i:[{t:3}],s:"Chargement..."}},saving:{t:0,b:{t:2,i:[{t:3}],s:"Enregistrement..."}},error:{t:0,b:{t:2,i:[{t:3}],s:"Une erreur est survenue"}},retry:{t:0,b:{t:2,i:[{t:3}],s:"Réessayer"}},save:{t:0,b:{t:2,i:[{t:3}],s:"Enregistrer"}},saving_data:{t:0,b:{t:2,i:[{t:3}],s:"Enregistrement des données..."}},cancel:{t:0,b:{t:2,i:[{t:3}],s:"Annuler"}},discard:{t:0,b:{t:2,i:[{t:3}],s:"Abandonner"}},delete:{t:0,b:{t:2,i:[{t:3}],s:"Supprimer"}},delete_confirm:{t:0,b:{t:2,i:[{t:3}],s:"Êtes-vous sûr de vouloir supprimer cet élément ?"}},delete_btn:{t:0,b:{t:2,i:[{t:3}],s:"Supprimer"}},delete_title:{t:0,b:{t:2,i:[{t:3}],s:"Confirmer la suppression"}},deleted:{t:0,b:{t:2,i:[{t:3}],s:"Supprimé avec succès"}},edit:{t:0,b:{t:2,i:[{t:3}],s:"Modifier"}},edit_title:{t:0,b:{t:2,i:[{t:3}],s:"Modifier l'élément"}},create:{t:0,b:{t:2,i:[{t:3}],s:"Créer"}},create_btn:{t:0,b:{t:2,i:[{t:3}],s:"Créer"}},create_title:{t:0,b:{t:2,i:[{t:3}],s:"Créer nouveau"}},create_first:{t:0,b:{t:2,i:[{t:3}],s:"Créez votre premier élément pour commencer"}},search:{t:0,b:{t:2,i:[{t:3}],s:"Rechercher"}},search_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Rechercher..."}},filter:{t:0,b:{t:2,i:[{t:3}],s:"Filtrer"}},no_results:{t:0,b:{t:2,i:[{t:3}],s:"Aucun résultat trouvé"}},no_results_hint:{t:0,b:{t:2,i:[{t:3}],s:"Essayez d'ajuster vos critères de recherche ou de filtre"}},no_items:{t:0,b:{t:2,i:[{t:3}],s:"Aucun élément disponible"}},actions:{t:0,b:{t:2,i:[{t:3}],s:"Actions"}},status:{t:0,b:{t:2,i:[{t:3}],s:"Statut"}},active:{t:0,b:{t:2,i:[{t:3}],s:"Actif"}},inactive:{t:0,b:{t:2,i:[{t:3}],s:"Inactif"}},activate:{t:0,b:{t:2,i:[{t:3}],s:"Activer"}},deactivate:{t:0,b:{t:2,i:[{t:3}],s:"Désactiver"}},name:{t:0,b:{t:2,i:[{t:3}],s:"Nom"}},name_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Entrez le nom"}},description:{t:0,b:{t:2,i:[{t:3}],s:"Description"}},desc_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Entrez la description"}},details:{t:0,b:{t:2,i:[{t:3}],s:"Détails"}},email:{t:0,b:{t:2,i:[{t:3}],s:"E-mail"}},id:{t:0,b:{t:2,i:[{t:3}],s:"ID"}},type:{t:0,b:{t:2,i:[{t:3}],s:"Type"}},type_corporate:{t:0,b:{t:2,i:[{t:3}],s:"Entreprise"}},view:{t:0,b:{t:2,i:[{t:3}],s:"Voir"}},updated:{t:0,b:{t:2,i:[{t:3}],s:"Mis à jour avec succès"}},save_error:{t:0,b:{t:2,i:[{t:3}],s:"Échec de l'enregistrement"}},invalid_json:{t:0,b:{t:2,i:[{t:3}],s:"Format JSON invalide"}},prev:{t:0,b:{t:2,i:[{t:3}],s:"Précédent"}},next:{t:0,b:{t:2,i:[{t:3}],s:"Suivant"}},none:{t:0,b:{t:2,i:[{t:3}],s:"Aucun"}},reset:{t:0,b:{t:2,i:[{t:3}],s:"Réinitialiser"}},confirm:{t:0,b:{t:2,i:[{t:3}],s:"Confirmer"}},close:{t:0,b:{t:2,i:[{t:3}],s:"Fermer"}},back:{t:0,b:{t:2,i:[{t:3}],s:"Retour"}},continue:{t:0,b:{t:2,i:[{t:3}],s:"Continuer"}},exit:{t:0,b:{t:2,i:[{t:3}],s:"Quitter"}},all:{t:0,b:{t:2,i:[{t:3}],s:"Tout"}},select_all:{t:0,b:{t:2,i:[{t:3}],s:"Tout sélectionner"}},deselect_all:{t:0,b:{t:2,i:[{t:3}],s:"Tout désélectionner"}},expires:{t:0,b:{t:2,i:[{t:3}],s:"Expire"}},created:{t:0,b:{t:2,i:[{t:3}],s:"Créé"}},start_date:{t:0,b:{t:2,i:[{t:3}],s:"Date de début"}},end_date:{t:0,b:{t:2,i:[{t:3}],s:"Date de fin"}},points:{t:0,b:{t:2,i:[{t:3}],s:"Points"}},level:{t:0,b:{t:2,i:[{t:3}],s:"Niveau"}},user:{t:0,b:{t:2,i:[{t:3}],s:"Utilisateur"}},user_id:{t:0,b:{t:2,i:[{t:3}],s:"ID utilisateur"}},services:{t:0,b:{t:2,i:[{t:3}],s:"Services"}},discoveries:{t:0,b:{t:2,i:[{t:3}],s:"Découvertes"}},restriction:{t:0,b:{t:2,i:[{t:3}],s:"Restriction"}},restriction_mild:{t:0,b:{t:2,i:[{t:3}],s:"Légère"}},restriction_moderate:{t:0,b:{t:2,i:[{t:3}],s:"Modérée"}},restriction_severe:{t:0,b:{t:2,i:[{t:3}],s:"Sévère"}},min_xp:{t:0,b:{t:2,i:[{t:3}],s:"XP min"}},xp:{t:0,b:{t:2,i:[{t:3}],s:"XP"}},penalty:{t:0,b:{t:2,i:[{t:3}],s:"Pénalité"}},feature_flags:{t:0,b:{t:2,i:[{t:3}],s:"Indicateurs de fonctionnalités"}},permission:{t:0,b:{t:2,i:[{t:3}],s:"Permission"}},role:{t:0,b:{t:2,i:[{t:3}],s:"Rôle"}},domain:{t:0,b:{t:2,i:[{t:3}],s:"Domaine"}},action:{t:0,b:{t:2,i:[{t:3}],s:"Action"}},value:{t:0,b:{t:2,i:[{t:3}],s:"Valeur"}},key:{t:0,b:{t:2,i:[{t:3}],s:"Clé"}},enabled:{t:0,b:{t:2,i:[{t:3}],s:"Activé"}},disabled:{t:0,b:{t:2,i:[{t:3}],s:"Désactivé"}},yes:{t:0,b:{t:2,i:[{t:3}],s:"Oui"}},no:{t:0,b:{t:2,i:[{t:3}],s:"Non"}},on:{t:0,b:{t:2,i:[{t:3}],s:"On"}},off:{t:0,b:{t:2,i:[{t:3}],s:"Off"}}}};export{t as default};

View File

@@ -1 +1 @@
import{Q as a,h as i,G as o,s as l,R as c}from"./BMplCfh_.js";const h=a(async(s,f)=>{let t,r;if(s.path==="/login")return;if(!i("access_token").value)return o("/login");const e=l();if(!e.user)try{[t,r]=c(()=>e.fetchUser()),await t,r()}catch{return e.logout(),o("/login")}if(!e.user)return e.logout(),o("/login");const n=["SUPERADMIN","ADMIN","MODERATOR","SALES_REP","SERVICE_MGR"],u=(e.user.role||"").toUpperCase();if(!n.includes(u))return e.logout(),o("/login")});export{h as default}; import{S as a,h as i,G as o,s as l,U as c}from"./Duy9QquX.js";const h=a(async(s,f)=>{let t,r;if(s.path==="/login")return;if(!i("access_token").value)return o("/login");const e=l();if(!e.user)try{[t,r]=c(()=>e.fetchUser()),await t,r()}catch{return e.logout(),o("/login")}if(!e.user)return e.logout(),o("/login");const n=["SUPERADMIN","ADMIN","MODERATOR","SALES_REP","SERVICE_MGR"],u=(e.user.role||"").toUpperCase();if(!n.includes(u))return e.logout(),o("/login")});export{h as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{_ as s}from"./DlAUqK2U.js";import{o,c as t,S as r}from"./BMplCfh_.js";const c={},n={class:"bg-slate-900 min-h-screen"};function a(e,l){return o(),t("div",n,[r(e.$slots,"default")])}const d=s(c,[["render",a]]);export{d as default};

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{_ as y}from"./15rOo9Uq.js";import{p as v,e as h,q as _,s as k,c as r,a as t,v as S,i as s,t as c,j as u,x as V,y as p,z as m,A as x,b as L,w as C,l as f,o as i,d as N}from"./BMplCfh_.js";const A=v("/sf_logo.png"),B={class:"min-h-screen bg-slate-900 flex items-center justify-center px-4"},E={class:"w-full max-w-md"},R={class:"bg-slate-800 rounded-xl shadow-2xl border border-slate-700 p-8"},j={key:0,class:"mb-6 p-3 rounded-lg bg-red-900/40 border border-red-700 text-red-300 text-sm"},q=["disabled"],D={key:0,class:"animate-spin h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},F={class:"mt-6 text-center"},z=h({__name:"login",setup(I){const a=f(""),l=f(""),b=_(),o=k();async function g(){try{await o.login(a.value,l.value),b.push("/")}catch(d){console.error("Login failed:",d)}}return(d,e)=>{const w=y;return i(),r("div",B,[t("div",E,[e[6]||(e[6]=S('<div class="text-center mb-8"><div class="flex items-center justify-center gap-3 mb-4"><img src="'+A+'" class="h-12 w-auto drop-shadow-md" alt="ServiceFinder Logo"><span class="text-3xl font-extrabold tracking-wider"><span class="text-white">SERVICE</span><span class="text-cyan-400">FINDER</span></span></div><h1 class="text-xl font-semibold text-slate-300">Staff Portal</h1><p class="text-sm text-slate-500 mt-1">Authorised personnel only</p></div>',1)),t("div",R,[s(o).error?(i(),r("div",j,c(s(o).error),1)):u("",!0),t("form",{onSubmit:V(g,["prevent"]),class:"space-y-5"},[t("div",null,[e[2]||(e[2]=t("label",{for:"email",class:"block text-sm font-medium text-slate-300 mb-1.5"}," Email Address ",-1)),p(t("input",{id:"email","onUpdate:modelValue":e[0]||(e[0]=n=>x(a)?a.value=n:null),type:"email",autocomplete:"email",required:"",placeholder:"admin@example.com",class:"w-full px-4 py-2.5 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition"},null,512),[[m,s(a)]])]),t("div",null,[e[3]||(e[3]=t("label",{for:"password",class:"block text-sm font-medium text-slate-300 mb-1.5"}," Password ",-1)),p(t("input",{id:"password","onUpdate:modelValue":e[1]||(e[1]=n=>x(l)?l.value=n:null),type:"password",autocomplete:"current-password",required:"",placeholder:"••••••••",class:"w-full px-4 py-2.5 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition"},null,512),[[m,s(l)]])]),t("button",{type:"submit",disabled:s(o).isLoading,class:"w-full py-2.5 px-4 bg-cyan-600 hover:bg-cyan-500 disabled:bg-cyan-800 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition duration-200 flex items-center justify-center gap-2"},[s(o).isLoading?(i(),r("svg",D,[...e[4]||(e[4]=[t("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor","stroke-width":"4"},null,-1),t("path",{class:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"},null,-1)])])):u("",!0),t("span",null,c(s(o).isLoading?"Signing in...":"Sign In"),1)],8,q)],32),t("div",F,[L(w,{to:"/",class:"text-sm text-slate-500 hover:text-cyan-400 transition"},{default:C(()=>[...e[5]||(e[5]=[N(" ← Back to ServiceFinder ",-1)])]),_:1})])])])])}}});export{z as default}; import{_ as y}from"./CuTzR9Y8.js";import{p as v,e as h,q as _,s as k,c as r,a as t,v as S,i as s,t as c,j as u,x as V,y as p,z as m,A as x,b as L,w as C,l as f,o as i,d as N}from"./Duy9QquX.js";const A=v("/sf_logo.png"),B={class:"min-h-screen bg-slate-900 flex items-center justify-center px-4"},E={class:"w-full max-w-md"},R={class:"bg-slate-800 rounded-xl shadow-2xl border border-slate-700 p-8"},j={key:0,class:"mb-6 p-3 rounded-lg bg-red-900/40 border border-red-700 text-red-300 text-sm"},q=["disabled"],D={key:0,class:"animate-spin h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},F={class:"mt-6 text-center"},z=h({__name:"login",setup(I){const a=f(""),l=f(""),b=_(),o=k();async function g(){try{await o.login(a.value,l.value),b.push("/")}catch(d){console.error("Login failed:",d)}}return(d,e)=>{const w=y;return i(),r("div",B,[t("div",E,[e[6]||(e[6]=S('<div class="text-center mb-8"><div class="flex items-center justify-center gap-3 mb-4"><img src="'+A+'" class="h-12 w-auto drop-shadow-md" alt="ServiceFinder Logo"><span class="text-3xl font-extrabold tracking-wider"><span class="text-white">SERVICE</span><span class="text-cyan-400">FINDER</span></span></div><h1 class="text-xl font-semibold text-slate-300">Staff Portal</h1><p class="text-sm text-slate-500 mt-1">Authorised personnel only</p></div>',1)),t("div",R,[s(o).error?(i(),r("div",j,c(s(o).error),1)):u("",!0),t("form",{onSubmit:V(g,["prevent"]),class:"space-y-5"},[t("div",null,[e[2]||(e[2]=t("label",{for:"email",class:"block text-sm font-medium text-slate-300 mb-1.5"}," Email Address ",-1)),p(t("input",{id:"email","onUpdate:modelValue":e[0]||(e[0]=n=>x(a)?a.value=n:null),type:"email",autocomplete:"email",required:"",placeholder:"admin@example.com",class:"w-full px-4 py-2.5 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition"},null,512),[[m,s(a)]])]),t("div",null,[e[3]||(e[3]=t("label",{for:"password",class:"block text-sm font-medium text-slate-300 mb-1.5"}," Password ",-1)),p(t("input",{id:"password","onUpdate:modelValue":e[1]||(e[1]=n=>x(l)?l.value=n:null),type:"password",autocomplete:"current-password",required:"",placeholder:"••••••••",class:"w-full px-4 py-2.5 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition"},null,512),[[m,s(l)]])]),t("button",{type:"submit",disabled:s(o).isLoading,class:"w-full py-2.5 px-4 bg-cyan-600 hover:bg-cyan-500 disabled:bg-cyan-800 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition duration-200 flex items-center justify-center gap-2"},[s(o).isLoading?(i(),r("svg",D,[...e[4]||(e[4]=[t("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor","stroke-width":"4"},null,-1),t("path",{class:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"},null,-1)])])):u("",!0),t("span",null,c(s(o).isLoading?"Signing in...":"Sign In"),1)],8,q)],32),t("div",F,[L(w,{to:"/",class:"text-sm text-slate-500 hover:text-cyan-400 transition"},{default:C(()=>[...e[5]||(e[5]=[N(" ← Back to ServiceFinder ",-1)])]),_:1})])])])])}}});export{z as default};

View File

@@ -0,0 +1 @@
const t={common:{loading:{t:0,b:{t:2,i:[{t:3}],s:"Betöltés..."}},saving:{t:0,b:{t:2,i:[{t:3}],s:"Mentés..."}},error:{t:0,b:{t:2,i:[{t:3}],s:"Hiba történt"}},retry:{t:0,b:{t:2,i:[{t:3}],s:"Újra"}},save:{t:0,b:{t:2,i:[{t:3}],s:"Mentés"}},saving_data:{t:0,b:{t:2,i:[{t:3}],s:"Adatok mentése..."}},cancel:{t:0,b:{t:2,i:[{t:3}],s:"Mégsem"}},discard:{t:0,b:{t:2,i:[{t:3}],s:"Elvetés"}},delete:{t:0,b:{t:2,i:[{t:3}],s:"Törlés"}},delete_confirm:{t:0,b:{t:2,i:[{t:3}],s:"Biztosan törölni szeretnéd ezt az elemet?"}},delete_btn:{t:0,b:{t:2,i:[{t:3}],s:"Törlés"}},delete_title:{t:0,b:{t:2,i:[{t:3}],s:"Törlés Megerősítése"}},deleted:{t:0,b:{t:2,i:[{t:3}],s:"Sikeresen törölve"}},edit:{t:0,b:{t:2,i:[{t:3}],s:"Szerkesztés"}},edit_title:{t:0,b:{t:2,i:[{t:3}],s:"Elem Szerkesztése"}},create:{t:0,b:{t:2,i:[{t:3}],s:"Létrehozás"}},create_btn:{t:0,b:{t:2,i:[{t:3}],s:"Létrehozás"}},create_title:{t:0,b:{t:2,i:[{t:3}],s:"Új Létrehozása"}},create_first:{t:0,b:{t:2,i:[{t:3}],s:"Hozd létre az első elemet a kezdéshez"}},search:{t:0,b:{t:2,i:[{t:3}],s:"Keresés"}},search_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Keresés..."}},filter:{t:0,b:{t:2,i:[{t:3}],s:"Szűrés"}},no_results:{t:0,b:{t:2,i:[{t:3}],s:"Nincs találat"}},no_results_hint:{t:0,b:{t:2,i:[{t:3}],s:"Próbáld módosítani a keresési vagy szűrési feltételeket"}},no_items:{t:0,b:{t:2,i:[{t:3}],s:"Nincsenek elemek"}},actions:{t:0,b:{t:2,i:[{t:3}],s:"Műveletek"}},status:{t:0,b:{t:2,i:[{t:3}],s:"Státusz"}},active:{t:0,b:{t:2,i:[{t:3}],s:"Aktív"}},inactive:{t:0,b:{t:2,i:[{t:3}],s:"Inaktív"}},activate:{t:0,b:{t:2,i:[{t:3}],s:"Aktiválás"}},deactivate:{t:0,b:{t:2,i:[{t:3}],s:"Deaktiválás"}},name:{t:0,b:{t:2,i:[{t:3}],s:"Név"}},name_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Add meg a nevet"}},description:{t:0,b:{t:2,i:[{t:3}],s:"Leírás"}},desc_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Add meg a leírást"}},details:{t:0,b:{t:2,i:[{t:3}],s:"Részletek"}},email:{t:0,b:{t:2,i:[{t:3}],s:"E-mail"}},id:{t:0,b:{t:2,i:[{t:3}],s:"Azonosító"}},type:{t:0,b:{t:2,i:[{t:3}],s:"Típus"}},type_corporate:{t:0,b:{t:2,i:[{t:3}],s:"Vállalati"}},view:{t:0,b:{t:2,i:[{t:3}],s:"Megtekintés"}},updated:{t:0,b:{t:2,i:[{t:3}],s:"Sikeresen frissítve"}},save_error:{t:0,b:{t:2,i:[{t:3}],s:"Sikertelen mentés"}},invalid_json:{t:0,b:{t:2,i:[{t:3}],s:"Érvénytelen JSON formátum"}},prev:{t:0,b:{t:2,i:[{t:3}],s:"Előző"}},next:{t:0,b:{t:2,i:[{t:3}],s:"Következő"}},none:{t:0,b:{t:2,i:[{t:3}],s:"Nincs"}},reset:{t:0,b:{t:2,i:[{t:3}],s:"Visszaállítás"}},confirm:{t:0,b:{t:2,i:[{t:3}],s:"Megerősítés"}},close:{t:0,b:{t:2,i:[{t:3}],s:"Bezárás"}},back:{t:0,b:{t:2,i:[{t:3}],s:"Vissza"}},continue:{t:0,b:{t:2,i:[{t:3}],s:"Tovább"}},exit:{t:0,b:{t:2,i:[{t:3}],s:"Kilépés"}},all:{t:0,b:{t:2,i:[{t:3}],s:"Minden"}},select_all:{t:0,b:{t:2,i:[{t:3}],s:"Összes kiválasztása"}},deselect_all:{t:0,b:{t:2,i:[{t:3}],s:"Összes elrejtése"}},expires:{t:0,b:{t:2,i:[{t:3}],s:"Lejárat"}},created:{t:0,b:{t:2,i:[{t:3}],s:"Létrehozva"}},start_date:{t:0,b:{t:2,i:[{t:3}],s:"Kezdő Dátum"}},end_date:{t:0,b:{t:2,i:[{t:3}],s:"Befejező Dátum"}},points:{t:0,b:{t:2,i:[{t:3}],s:"Pontok"}},level:{t:0,b:{t:2,i:[{t:3}],s:"Szint"}},user:{t:0,b:{t:2,i:[{t:3}],s:"Felhasználó"}},user_id:{t:0,b:{t:2,i:[{t:3}],s:"Felhasználó Azonosító"}},services:{t:0,b:{t:2,i:[{t:3}],s:"Szolgáltatások"}},discoveries:{t:0,b:{t:2,i:[{t:3}],s:"Felfedezések"}},restriction:{t:0,b:{t:2,i:[{t:3}],s:"Korlátozás"}},restriction_mild:{t:0,b:{t:2,i:[{t:3}],s:"Enyhe"}},restriction_moderate:{t:0,b:{t:2,i:[{t:3}],s:"Mérsékelt"}},restriction_severe:{t:0,b:{t:2,i:[{t:3}],s:"Súlyos"}},min_xp:{t:0,b:{t:2,i:[{t:3}],s:"Min XP"}},xp:{t:0,b:{t:2,i:[{t:3}],s:"XP"}},penalty:{t:0,b:{t:2,i:[{t:3}],s:"Büntetés"}},feature_flags:{t:0,b:{t:2,i:[{t:3}],s:"Funkció Kapcsolók"}},permission:{t:0,b:{t:2,i:[{t:3}],s:"Engedély"}},role:{t:0,b:{t:2,i:[{t:3}],s:"Szerepkör"}},domain:{t:0,b:{t:2,i:[{t:3}],s:"Tartomány"}},action:{t:0,b:{t:2,i:[{t:3}],s:"Művelet"}},value:{t:0,b:{t:2,i:[{t:3}],s:"Érték"}},key:{t:0,b:{t:2,i:[{t:3}],s:"Kulcs"}},enabled:{t:0,b:{t:2,i:[{t:3}],s:"Bekapcsolva"}},disabled:{t:0,b:{t:2,i:[{t:3}],s:"Kikapcsolva"}},yes:{t:0,b:{t:2,i:[{t:3}],s:"Igen"}},no:{t:0,b:{t:2,i:[{t:3}],s:"Nem"}},on:{t:0,b:{t:2,i:[{t:3}],s:"Be"}},off:{t:0,b:{t:2,i:[{t:3}],s:"Ki"}}}};export{t as default};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{_ as s}from"./DlAUqK2U.js";import{o,c as t,V as r}from"./Duy9QquX.js";const c={},n={class:"bg-slate-900 min-h-screen"};function a(e,l){return o(),t("div",n,[r(e.$slots,"default")])}const d=s(c,[["render",a]]);export{d as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{_ as s}from"./DlAUqK2U.js";import{u as a,o as i,c as u,a as e,t as o}from"./BMplCfh_.js";const l={class:"antialiased bg-white dark:bg-black dark:text-white font-sans grid min-h-screen overflow-hidden place-content-center text-black"},c={class:"max-w-520px text-center"},d=["textContent"],p=["textContent"],f={__name:"error-500",props:{appName:{type:String,default:"Nuxt"},version:{type:String,default:""},status:{type:Number,default:500},statusText:{type:String,default:"Server error"},description:{type:String,default:"This page is temporarily unavailable."}},setup(t){const r=t;return a({title:`${r.status} - ${r.statusText} | ${r.appName}`,script:[{innerHTML:`!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)}).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`}],style:[{innerHTML:'*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1{font-size:inherit;font-weight:inherit}h1,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(g,n)=>(i(),u("div",l,[n[0]||(n[0]=e("div",{class:"-bottom-1/2 fixed h-1/2 left-0 right-0 spotlight"},null,-1)),e("div",c,[e("h1",{class:"font-medium mb-8 sm:text-10xl text-8xl",textContent:o(t.status)},null,8,d),e("p",{class:"font-light leading-tight mb-16 px-8 sm:px-0 sm:text-4xl text-xl",textContent:o(t.description)},null,8,p)])]))}},h=s(f,[["__scopeId","data-v-a01dd0ba"]]);export{h as default}; import{_ as s}from"./DlAUqK2U.js";import{u as a,o as i,c as u,a as e,t as o}from"./Duy9QquX.js";const l={class:"antialiased bg-white dark:bg-black dark:text-white font-sans grid min-h-screen overflow-hidden place-content-center text-black"},c={class:"max-w-520px text-center"},d=["textContent"],p=["textContent"],f={__name:"error-500",props:{appName:{type:String,default:"Nuxt"},version:{type:String,default:""},status:{type:Number,default:500},statusText:{type:String,default:"Server error"},description:{type:String,default:"This page is temporarily unavailable."}},setup(t){const r=t;return a({title:`${r.status} - ${r.statusText} | ${r.appName}`,script:[{innerHTML:`!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)}).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`}],style:[{innerHTML:'*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1{font-size:inherit;font-weight:inherit}h1,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(g,n)=>(i(),u("div",l,[n[0]||(n[0]=e("div",{class:"-bottom-1/2 fixed h-1/2 left-0 right-0 spotlight"},null,-1)),e("div",c,[e("h1",{class:"font-medium mb-8 sm:text-10xl text-8xl",textContent:o(t.status)},null,8,d),e("p",{class:"font-light leading-tight mb-16 px-8 sm:px-0 sm:text-4xl text-xl",textContent:o(t.description)},null,8,p)])]))}},h=s(f,[["__scopeId","data-v-a01dd0ba"]]);export{h as default};

View File

@@ -0,0 +1 @@
const t={common:{loading:{t:0,b:{t:2,i:[{t:3}],s:"Wird geladen..."}},saving:{t:0,b:{t:2,i:[{t:3}],s:"Wird gespeichert..."}},error:{t:0,b:{t:2,i:[{t:3}],s:"Ein Fehler ist aufgetreten"}},retry:{t:0,b:{t:2,i:[{t:3}],s:"Wiederholen"}},save:{t:0,b:{t:2,i:[{t:3}],s:"Speichern"}},saving_data:{t:0,b:{t:2,i:[{t:3}],s:"Daten werden gespeichert..."}},cancel:{t:0,b:{t:2,i:[{t:3}],s:"Abbrechen"}},discard:{t:0,b:{t:2,i:[{t:3}],s:"Verwerfen"}},delete:{t:0,b:{t:2,i:[{t:3}],s:"Löschen"}},delete_confirm:{t:0,b:{t:2,i:[{t:3}],s:"Sind Sie sicher, dass Sie dieses Element löschen möchten?"}},delete_btn:{t:0,b:{t:2,i:[{t:3}],s:"Löschen"}},delete_title:{t:0,b:{t:2,i:[{t:3}],s:"Löschen bestätigen"}},deleted:{t:0,b:{t:2,i:[{t:3}],s:"Erfolgreich gelöscht"}},edit:{t:0,b:{t:2,i:[{t:3}],s:"Bearbeiten"}},edit_title:{t:0,b:{t:2,i:[{t:3}],s:"Element bearbeiten"}},create:{t:0,b:{t:2,i:[{t:3}],s:"Erstellen"}},create_btn:{t:0,b:{t:2,i:[{t:3}],s:"Erstellen"}},create_title:{t:0,b:{t:2,i:[{t:3}],s:"Neu erstellen"}},create_first:{t:0,b:{t:2,i:[{t:3}],s:"Erstellen Sie Ihr erstes Element, um zu beginnen"}},search:{t:0,b:{t:2,i:[{t:3}],s:"Suchen"}},search_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Suchen..."}},filter:{t:0,b:{t:2,i:[{t:3}],s:"Filter"}},no_results:{t:0,b:{t:2,i:[{t:3}],s:"Keine Ergebnisse gefunden"}},no_results_hint:{t:0,b:{t:2,i:[{t:3}],s:"Versuchen Sie, Ihre Such- oder Filterkriterien anzupassen"}},no_items:{t:0,b:{t:2,i:[{t:3}],s:"Keine Elemente verfügbar"}},actions:{t:0,b:{t:2,i:[{t:3}],s:"Aktionen"}},status:{t:0,b:{t:2,i:[{t:3}],s:"Status"}},active:{t:0,b:{t:2,i:[{t:3}],s:"Aktiv"}},inactive:{t:0,b:{t:2,i:[{t:3}],s:"Inaktiv"}},activate:{t:0,b:{t:2,i:[{t:3}],s:"Aktivieren"}},deactivate:{t:0,b:{t:2,i:[{t:3}],s:"Deaktivieren"}},name:{t:0,b:{t:2,i:[{t:3}],s:"Name"}},name_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Namen eingeben"}},description:{t:0,b:{t:2,i:[{t:3}],s:"Beschreibung"}},desc_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Beschreibung eingeben"}},details:{t:0,b:{t:2,i:[{t:3}],s:"Details"}},email:{t:0,b:{t:2,i:[{t:3}],s:"E-Mail"}},id:{t:0,b:{t:2,i:[{t:3}],s:"ID"}},type:{t:0,b:{t:2,i:[{t:3}],s:"Typ"}},type_corporate:{t:0,b:{t:2,i:[{t:3}],s:"Unternehmen"}},view:{t:0,b:{t:2,i:[{t:3}],s:"Ansehen"}},updated:{t:0,b:{t:2,i:[{t:3}],s:"Erfolgreich aktualisiert"}},save_error:{t:0,b:{t:2,i:[{t:3}],s:"Speichern fehlgeschlagen"}},invalid_json:{t:0,b:{t:2,i:[{t:3}],s:"Ungültiges JSON-Format"}},prev:{t:0,b:{t:2,i:[{t:3}],s:"Vorherige"}},next:{t:0,b:{t:2,i:[{t:3}],s:"Nächste"}},none:{t:0,b:{t:2,i:[{t:3}],s:"Keine"}},reset:{t:0,b:{t:2,i:[{t:3}],s:"Zurücksetzen"}},confirm:{t:0,b:{t:2,i:[{t:3}],s:"Bestätigen"}},close:{t:0,b:{t:2,i:[{t:3}],s:"Schließen"}},back:{t:0,b:{t:2,i:[{t:3}],s:"Zurück"}},continue:{t:0,b:{t:2,i:[{t:3}],s:"Fortfahren"}},exit:{t:0,b:{t:2,i:[{t:3}],s:"Beenden"}},all:{t:0,b:{t:2,i:[{t:3}],s:"Alle"}},select_all:{t:0,b:{t:2,i:[{t:3}],s:"Alle auswählen"}},deselect_all:{t:0,b:{t:2,i:[{t:3}],s:"Alle abwählen"}},expires:{t:0,b:{t:2,i:[{t:3}],s:"Läuft ab"}},created:{t:0,b:{t:2,i:[{t:3}],s:"Erstellt"}},start_date:{t:0,b:{t:2,i:[{t:3}],s:"Startdatum"}},end_date:{t:0,b:{t:2,i:[{t:3}],s:"Enddatum"}},points:{t:0,b:{t:2,i:[{t:3}],s:"Punkte"}},level:{t:0,b:{t:2,i:[{t:3}],s:"Stufe"}},user:{t:0,b:{t:2,i:[{t:3}],s:"Benutzer"}},user_id:{t:0,b:{t:2,i:[{t:3}],s:"Benutzer-ID"}},services:{t:0,b:{t:2,i:[{t:3}],s:"Dienstleistungen"}},discoveries:{t:0,b:{t:2,i:[{t:3}],s:"Entdeckungen"}},restriction:{t:0,b:{t:2,i:[{t:3}],s:"Einschränkung"}},restriction_mild:{t:0,b:{t:2,i:[{t:3}],s:"Mild"}},restriction_moderate:{t:0,b:{t:2,i:[{t:3}],s:"Moderat"}},restriction_severe:{t:0,b:{t:2,i:[{t:3}],s:"Schwer"}},min_xp:{t:0,b:{t:2,i:[{t:3}],s:"Min. XP"}},xp:{t:0,b:{t:2,i:[{t:3}],s:"XP"}},penalty:{t:0,b:{t:2,i:[{t:3}],s:"Strafe"}},feature_flags:{t:0,b:{t:2,i:[{t:3}],s:"Funktionsschalter"}},permission:{t:0,b:{t:2,i:[{t:3}],s:"Berechtigung"}},role:{t:0,b:{t:2,i:[{t:3}],s:"Rolle"}},domain:{t:0,b:{t:2,i:[{t:3}],s:"Domäne"}},action:{t:0,b:{t:2,i:[{t:3}],s:"Aktion"}},value:{t:0,b:{t:2,i:[{t:3}],s:"Wert"}},key:{t:0,b:{t:2,i:[{t:3}],s:"Schlüssel"}},enabled:{t:0,b:{t:2,i:[{t:3}],s:"Aktiviert"}},disabled:{t:0,b:{t:2,i:[{t:3}],s:"Deaktiviert"}},yes:{t:0,b:{t:2,i:[{t:3}],s:"Ja"}},no:{t:0,b:{t:2,i:[{t:3}],s:"Nein"}},on:{t:0,b:{t:2,i:[{t:3}],s:"Ein"}},off:{t:0,b:{t:2,i:[{t:3}],s:"Aus"}}}};export{t as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
const t={common:{loading:{t:0,b:{t:2,i:[{t:3}],s:"Loading..."}},saving:{t:0,b:{t:2,i:[{t:3}],s:"Saving..."}},error:{t:0,b:{t:2,i:[{t:3}],s:"An error occurred"}},retry:{t:0,b:{t:2,i:[{t:3}],s:"Retry"}},save:{t:0,b:{t:2,i:[{t:3}],s:"Save"}},saving_data:{t:0,b:{t:2,i:[{t:3}],s:"Saving data..."}},cancel:{t:0,b:{t:2,i:[{t:3}],s:"Cancel"}},discard:{t:0,b:{t:2,i:[{t:3}],s:"Discard"}},delete:{t:0,b:{t:2,i:[{t:3}],s:"Delete"}},delete_confirm:{t:0,b:{t:2,i:[{t:3}],s:"Are you sure you want to delete this item?"}},delete_btn:{t:0,b:{t:2,i:[{t:3}],s:"Delete"}},delete_title:{t:0,b:{t:2,i:[{t:3}],s:"Confirm Deletion"}},deleted:{t:0,b:{t:2,i:[{t:3}],s:"Deleted successfully"}},edit:{t:0,b:{t:2,i:[{t:3}],s:"Edit"}},edit_title:{t:0,b:{t:2,i:[{t:3}],s:"Edit Item"}},create:{t:0,b:{t:2,i:[{t:3}],s:"Create"}},create_btn:{t:0,b:{t:2,i:[{t:3}],s:"Create"}},create_title:{t:0,b:{t:2,i:[{t:3}],s:"Create New"}},create_first:{t:0,b:{t:2,i:[{t:3}],s:"Create your first item to get started"}},search:{t:0,b:{t:2,i:[{t:3}],s:"Search"}},search_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Search..."}},filter:{t:0,b:{t:2,i:[{t:3}],s:"Filter"}},no_results:{t:0,b:{t:2,i:[{t:3}],s:"No results found"}},no_results_hint:{t:0,b:{t:2,i:[{t:3}],s:"Try adjusting your search or filter criteria"}},no_items:{t:0,b:{t:2,i:[{t:3}],s:"No items available"}},actions:{t:0,b:{t:2,i:[{t:3}],s:"Actions"}},status:{t:0,b:{t:2,i:[{t:3}],s:"Status"}},active:{t:0,b:{t:2,i:[{t:3}],s:"Active"}},inactive:{t:0,b:{t:2,i:[{t:3}],s:"Inactive"}},activate:{t:0,b:{t:2,i:[{t:3}],s:"Activate"}},deactivate:{t:0,b:{t:2,i:[{t:3}],s:"Deactivate"}},name:{t:0,b:{t:2,i:[{t:3}],s:"Name"}},name_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Enter name"}},description:{t:0,b:{t:2,i:[{t:3}],s:"Description"}},desc_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Enter description"}},details:{t:0,b:{t:2,i:[{t:3}],s:"Details"}},email:{t:0,b:{t:2,i:[{t:3}],s:"Email"}},id:{t:0,b:{t:2,i:[{t:3}],s:"ID"}},type:{t:0,b:{t:2,i:[{t:3}],s:"Type"}},type_corporate:{t:0,b:{t:2,i:[{t:3}],s:"Corporate"}},view:{t:0,b:{t:2,i:[{t:3}],s:"View"}},updated:{t:0,b:{t:2,i:[{t:3}],s:"Updated successfully"}},save_error:{t:0,b:{t:2,i:[{t:3}],s:"Failed to save"}},invalid_json:{t:0,b:{t:2,i:[{t:3}],s:"Invalid JSON format"}},prev:{t:0,b:{t:2,i:[{t:3}],s:"Previous"}},next:{t:0,b:{t:2,i:[{t:3}],s:"Next"}},none:{t:0,b:{t:2,i:[{t:3}],s:"None"}},reset:{t:0,b:{t:2,i:[{t:3}],s:"Reset"}},confirm:{t:0,b:{t:2,i:[{t:3}],s:"Confirm"}},close:{t:0,b:{t:2,i:[{t:3}],s:"Close"}},back:{t:0,b:{t:2,i:[{t:3}],s:"Back"}},continue:{t:0,b:{t:2,i:[{t:3}],s:"Continue"}},exit:{t:0,b:{t:2,i:[{t:3}],s:"Exit"}},all:{t:0,b:{t:2,i:[{t:3}],s:"All"}},select_all:{t:0,b:{t:2,i:[{t:3}],s:"Select All"}},deselect_all:{t:0,b:{t:2,i:[{t:3}],s:"Deselect All"}},expires:{t:0,b:{t:2,i:[{t:3}],s:"Expires"}},created:{t:0,b:{t:2,i:[{t:3}],s:"Created"}},start_date:{t:0,b:{t:2,i:[{t:3}],s:"Start Date"}},end_date:{t:0,b:{t:2,i:[{t:3}],s:"End Date"}},points:{t:0,b:{t:2,i:[{t:3}],s:"Points"}},level:{t:0,b:{t:2,i:[{t:3}],s:"Level"}},user:{t:0,b:{t:2,i:[{t:3}],s:"User"}},user_id:{t:0,b:{t:2,i:[{t:3}],s:"User ID"}},services:{t:0,b:{t:2,i:[{t:3}],s:"Services"}},discoveries:{t:0,b:{t:2,i:[{t:3}],s:"Discoveries"}},restriction:{t:0,b:{t:2,i:[{t:3}],s:"Restriction"}},restriction_mild:{t:0,b:{t:2,i:[{t:3}],s:"Mild"}},restriction_moderate:{t:0,b:{t:2,i:[{t:3}],s:"Moderate"}},restriction_severe:{t:0,b:{t:2,i:[{t:3}],s:"Severe"}},min_xp:{t:0,b:{t:2,i:[{t:3}],s:"Min XP"}},xp:{t:0,b:{t:2,i:[{t:3}],s:"XP"}},penalty:{t:0,b:{t:2,i:[{t:3}],s:"Penalty"}},feature_flags:{t:0,b:{t:2,i:[{t:3}],s:"Feature Flags"}},permission:{t:0,b:{t:2,i:[{t:3}],s:"Permission"}},role:{t:0,b:{t:2,i:[{t:3}],s:"Role"}},domain:{t:0,b:{t:2,i:[{t:3}],s:"Domain"}},action:{t:0,b:{t:2,i:[{t:3}],s:"Action"}},value:{t:0,b:{t:2,i:[{t:3}],s:"Value"}},key:{t:0,b:{t:2,i:[{t:3}],s:"Key"}},enabled:{t:0,b:{t:2,i:[{t:3}],s:"Enabled"}},disabled:{t:0,b:{t:2,i:[{t:3}],s:"Disabled"}},yes:{t:0,b:{t:2,i:[{t:3}],s:"Yes"}},no:{t:0,b:{t:2,i:[{t:3}],s:"No"}},on:{t:0,b:{t:2,i:[{t:3}],s:"On"}},off:{t:0,b:{t:2,i:[{t:3}],s:"Off"}}}};export{t as default};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
const t={common:{loading:{t:0,b:{t:2,i:[{t:3}],s:"Se încarcă..."}},saving:{t:0,b:{t:2,i:[{t:3}],s:"Se salvează..."}},error:{t:0,b:{t:2,i:[{t:3}],s:"A apărut o eroare"}},retry:{t:0,b:{t:2,i:[{t:3}],s:"Reîncercare"}},save:{t:0,b:{t:2,i:[{t:3}],s:"Salvează"}},saving_data:{t:0,b:{t:2,i:[{t:3}],s:"Se salvează datele..."}},cancel:{t:0,b:{t:2,i:[{t:3}],s:"Anulează"}},discard:{t:0,b:{t:2,i:[{t:3}],s:"Renunță"}},delete:{t:0,b:{t:2,i:[{t:3}],s:"Șterge"}},delete_confirm:{t:0,b:{t:2,i:[{t:3}],s:"Ești sigur că vrei să ștergi acest element?"}},delete_btn:{t:0,b:{t:2,i:[{t:3}],s:"Șterge"}},delete_title:{t:0,b:{t:2,i:[{t:3}],s:"Confirmă Ștergerea"}},deleted:{t:0,b:{t:2,i:[{t:3}],s:"Șters cu succes"}},edit:{t:0,b:{t:2,i:[{t:3}],s:"Editează"}},edit_title:{t:0,b:{t:2,i:[{t:3}],s:"Editează Elementul"}},create:{t:0,b:{t:2,i:[{t:3}],s:"Creează"}},create_btn:{t:0,b:{t:2,i:[{t:3}],s:"Creează"}},create_title:{t:0,b:{t:2,i:[{t:3}],s:"Creează Nou"}},create_first:{t:0,b:{t:2,i:[{t:3}],s:"Creează primul tău element pentru a începe"}},search:{t:0,b:{t:2,i:[{t:3}],s:"Caută"}},search_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Caută..."}},filter:{t:0,b:{t:2,i:[{t:3}],s:"Filtru"}},no_results:{t:0,b:{t:2,i:[{t:3}],s:"Niciun rezultat găsit"}},no_results_hint:{t:0,b:{t:2,i:[{t:3}],s:"Încearcă să ajustezi criteriile de căutare sau filtru"}},no_items:{t:0,b:{t:2,i:[{t:3}],s:"Niciun element disponibil"}},actions:{t:0,b:{t:2,i:[{t:3}],s:"Acțiuni"}},status:{t:0,b:{t:2,i:[{t:3}],s:"Status"}},active:{t:0,b:{t:2,i:[{t:3}],s:"Activ"}},inactive:{t:0,b:{t:2,i:[{t:3}],s:"Inactiv"}},activate:{t:0,b:{t:2,i:[{t:3}],s:"Activează"}},deactivate:{t:0,b:{t:2,i:[{t:3}],s:"Dezactivează"}},name:{t:0,b:{t:2,i:[{t:3}],s:"Nume"}},name_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Introdu numele"}},description:{t:0,b:{t:2,i:[{t:3}],s:"Descriere"}},desc_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Introdu descrierea"}},details:{t:0,b:{t:2,i:[{t:3}],s:"Detalii"}},email:{t:0,b:{t:2,i:[{t:3}],s:"Email"}},id:{t:0,b:{t:2,i:[{t:3}],s:"ID"}},type:{t:0,b:{t:2,i:[{t:3}],s:"Tip"}},type_corporate:{t:0,b:{t:2,i:[{t:3}],s:"Corporativ"}},view:{t:0,b:{t:2,i:[{t:3}],s:"Vizualizează"}},updated:{t:0,b:{t:2,i:[{t:3}],s:"Actualizat cu succes"}},save_error:{t:0,b:{t:2,i:[{t:3}],s:"Salvarea a eșuat"}},invalid_json:{t:0,b:{t:2,i:[{t:3}],s:"Format JSON invalid"}},prev:{t:0,b:{t:2,i:[{t:3}],s:"Anterior"}},next:{t:0,b:{t:2,i:[{t:3}],s:"Următor"}},none:{t:0,b:{t:2,i:[{t:3}],s:"Niciunul"}},reset:{t:0,b:{t:2,i:[{t:3}],s:"Resetează"}},confirm:{t:0,b:{t:2,i:[{t:3}],s:"Confirmă"}},close:{t:0,b:{t:2,i:[{t:3}],s:"Închide"}},back:{t:0,b:{t:2,i:[{t:3}],s:"Înapoi"}},continue:{t:0,b:{t:2,i:[{t:3}],s:"Continuă"}},exit:{t:0,b:{t:2,i:[{t:3}],s:"Ieșire"}},all:{t:0,b:{t:2,i:[{t:3}],s:"Toate"}},select_all:{t:0,b:{t:2,i:[{t:3}],s:"Selectează Tot"}},deselect_all:{t:0,b:{t:2,i:[{t:3}],s:"Deselectează Tot"}},expires:{t:0,b:{t:2,i:[{t:3}],s:"Expiră"}},created:{t:0,b:{t:2,i:[{t:3}],s:"Creat"}},start_date:{t:0,b:{t:2,i:[{t:3}],s:"Data Începerii"}},end_date:{t:0,b:{t:2,i:[{t:3}],s:"Data Încheierii"}},points:{t:0,b:{t:2,i:[{t:3}],s:"Puncte"}},level:{t:0,b:{t:2,i:[{t:3}],s:"Nivel"}},user:{t:0,b:{t:2,i:[{t:3}],s:"Utilizator"}},user_id:{t:0,b:{t:2,i:[{t:3}],s:"ID Utilizator"}},services:{t:0,b:{t:2,i:[{t:3}],s:"Servicii"}},discoveries:{t:0,b:{t:2,i:[{t:3}],s:"Descoperiri"}},restriction:{t:0,b:{t:2,i:[{t:3}],s:"Restricție"}},restriction_mild:{t:0,b:{t:2,i:[{t:3}],s:"Ușoară"}},restriction_moderate:{t:0,b:{t:2,i:[{t:3}],s:"Moderată"}},restriction_severe:{t:0,b:{t:2,i:[{t:3}],s:"Severă"}},min_xp:{t:0,b:{t:2,i:[{t:3}],s:"XP Min"}},xp:{t:0,b:{t:2,i:[{t:3}],s:"XP"}},penalty:{t:0,b:{t:2,i:[{t:3}],s:"Penalizare"}},feature_flags:{t:0,b:{t:2,i:[{t:3}],s:"Funcții"}},permission:{t:0,b:{t:2,i:[{t:3}],s:"Permisiune"}},role:{t:0,b:{t:2,i:[{t:3}],s:"Rol"}},domain:{t:0,b:{t:2,i:[{t:3}],s:"Domeniu"}},action:{t:0,b:{t:2,i:[{t:3}],s:"Acțiune"}},value:{t:0,b:{t:2,i:[{t:3}],s:"Valoare"}},key:{t:0,b:{t:2,i:[{t:3}],s:"Cheie"}},enabled:{t:0,b:{t:2,i:[{t:3}],s:"Activat"}},disabled:{t:0,b:{t:2,i:[{t:3}],s:"Dezactivat"}},yes:{t:0,b:{t:2,i:[{t:3}],s:"Da"}},no:{t:0,b:{t:2,i:[{t:3}],s:"Nu"}},on:{t:0,b:{t:2,i:[{t:3}],s:"Pornit"}},off:{t:0,b:{t:2,i:[{t:3}],s:"Oprit"}}}};export{t as default};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
const t={common:{loading:{t:0,b:{t:2,i:[{t:3}],s:"Načítání..."}},saving:{t:0,b:{t:2,i:[{t:3}],s:"Ukládání..."}},error:{t:0,b:{t:2,i:[{t:3}],s:"Došlo k chybě"}},retry:{t:0,b:{t:2,i:[{t:3}],s:"Opakovat"}},save:{t:0,b:{t:2,i:[{t:3}],s:"Uložit"}},saving_data:{t:0,b:{t:2,i:[{t:3}],s:"Ukládání dat..."}},cancel:{t:0,b:{t:2,i:[{t:3}],s:"Zrušit"}},discard:{t:0,b:{t:2,i:[{t:3}],s:"Zahodit"}},delete:{t:0,b:{t:2,i:[{t:3}],s:"Smazat"}},delete_confirm:{t:0,b:{t:2,i:[{t:3}],s:"Opravdu chcete smazat tuto položku?"}},delete_btn:{t:0,b:{t:2,i:[{t:3}],s:"Smazat"}},delete_title:{t:0,b:{t:2,i:[{t:3}],s:"Potvrdit Smazání"}},deleted:{t:0,b:{t:2,i:[{t:3}],s:"Úspěšně smazáno"}},edit:{t:0,b:{t:2,i:[{t:3}],s:"Upravit"}},edit_title:{t:0,b:{t:2,i:[{t:3}],s:"Upravit Položku"}},create:{t:0,b:{t:2,i:[{t:3}],s:"Vytvořit"}},create_btn:{t:0,b:{t:2,i:[{t:3}],s:"Vytvořit"}},create_title:{t:0,b:{t:2,i:[{t:3}],s:"Vytvořit Nové"}},create_first:{t:0,b:{t:2,i:[{t:3}],s:"Vytvořte svou první položku pro začátek"}},search:{t:0,b:{t:2,i:[{t:3}],s:"Hledat"}},search_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Hledat..."}},filter:{t:0,b:{t:2,i:[{t:3}],s:"Filtr"}},no_results:{t:0,b:{t:2,i:[{t:3}],s:"Nebyly nalezeny žádné výsledky"}},no_results_hint:{t:0,b:{t:2,i:[{t:3}],s:"Zkuste upravit kritéria vyhledávání nebo filtru"}},no_items:{t:0,b:{t:2,i:[{t:3}],s:"Žádné položky k dispozici"}},actions:{t:0,b:{t:2,i:[{t:3}],s:"Akce"}},status:{t:0,b:{t:2,i:[{t:3}],s:"Stav"}},active:{t:0,b:{t:2,i:[{t:3}],s:"Aktivní"}},inactive:{t:0,b:{t:2,i:[{t:3}],s:"Neaktivní"}},activate:{t:0,b:{t:2,i:[{t:3}],s:"Aktivovat"}},deactivate:{t:0,b:{t:2,i:[{t:3}],s:"Deaktivovat"}},name:{t:0,b:{t:2,i:[{t:3}],s:"Název"}},name_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Zadejte název"}},description:{t:0,b:{t:2,i:[{t:3}],s:"Popis"}},desc_placeholder:{t:0,b:{t:2,i:[{t:3}],s:"Zadejte popis"}},details:{t:0,b:{t:2,i:[{t:3}],s:"Detaily"}},email:{t:0,b:{t:2,i:[{t:3}],s:"Email"}},id:{t:0,b:{t:2,i:[{t:3}],s:"ID"}},type:{t:0,b:{t:2,i:[{t:3}],s:"Typ"}},type_corporate:{t:0,b:{t:2,i:[{t:3}],s:"Firemní"}},view:{t:0,b:{t:2,i:[{t:3}],s:"Zobrazit"}},updated:{t:0,b:{t:2,i:[{t:3}],s:"Úspěšně aktualizováno"}},save_error:{t:0,b:{t:2,i:[{t:3}],s:"Uložení selhalo"}},invalid_json:{t:0,b:{t:2,i:[{t:3}],s:"Neplatný formát JSON"}},prev:{t:0,b:{t:2,i:[{t:3}],s:"Předchozí"}},next:{t:0,b:{t:2,i:[{t:3}],s:"Další"}},none:{t:0,b:{t:2,i:[{t:3}],s:"Žádné"}},reset:{t:0,b:{t:2,i:[{t:3}],s:"Reset"}},confirm:{t:0,b:{t:2,i:[{t:3}],s:"Potvrdit"}},close:{t:0,b:{t:2,i:[{t:3}],s:"Zavřít"}},back:{t:0,b:{t:2,i:[{t:3}],s:"Zpět"}},continue:{t:0,b:{t:2,i:[{t:3}],s:"Pokračovat"}},exit:{t:0,b:{t:2,i:[{t:3}],s:"Ukončit"}},all:{t:0,b:{t:2,i:[{t:3}],s:"Vše"}},select_all:{t:0,b:{t:2,i:[{t:3}],s:"Vybrat Vše"}},deselect_all:{t:0,b:{t:2,i:[{t:3}],s:"Odznačit Vše"}},expires:{t:0,b:{t:2,i:[{t:3}],s:"Vyprší"}},created:{t:0,b:{t:2,i:[{t:3}],s:"Vytvořeno"}},start_date:{t:0,b:{t:2,i:[{t:3}],s:"Datum Zahájení"}},end_date:{t:0,b:{t:2,i:[{t:3}],s:"Datum Ukončení"}},points:{t:0,b:{t:2,i:[{t:3}],s:"Body"}},level:{t:0,b:{t:2,i:[{t:3}],s:"Úroveň"}},user:{t:0,b:{t:2,i:[{t:3}],s:"Uživatel"}},user_id:{t:0,b:{t:2,i:[{t:3}],s:"ID Uživatele"}},services:{t:0,b:{t:2,i:[{t:3}],s:"Služby"}},discoveries:{t:0,b:{t:2,i:[{t:3}],s:"Objevy"}},restriction:{t:0,b:{t:2,i:[{t:3}],s:"Omezení"}},restriction_mild:{t:0,b:{t:2,i:[{t:3}],s:"Mírné"}},restriction_moderate:{t:0,b:{t:2,i:[{t:3}],s:"Střední"}},restriction_severe:{t:0,b:{t:2,i:[{t:3}],s:"Přísné"}},min_xp:{t:0,b:{t:2,i:[{t:3}],s:"Min XP"}},xp:{t:0,b:{t:2,i:[{t:3}],s:"XP"}},penalty:{t:0,b:{t:2,i:[{t:3}],s:"Trest"}},feature_flags:{t:0,b:{t:2,i:[{t:3}],s:"Funkce"}},permission:{t:0,b:{t:2,i:[{t:3}],s:"Oprávnění"}},role:{t:0,b:{t:2,i:[{t:3}],s:"Role"}},domain:{t:0,b:{t:2,i:[{t:3}],s:"Doména"}},action:{t:0,b:{t:2,i:[{t:3}],s:"Akce"}},value:{t:0,b:{t:2,i:[{t:3}],s:"Hodnota"}},key:{t:0,b:{t:2,i:[{t:3}],s:"Klíč"}},enabled:{t:0,b:{t:2,i:[{t:3}],s:"Povoleno"}},disabled:{t:0,b:{t:2,i:[{t:3}],s:"Zakázáno"}},yes:{t:0,b:{t:2,i:[{t:3}],s:"Ano"}},no:{t:0,b:{t:2,i:[{t:3}],s:"Ne"}},on:{t:0,b:{t:2,i:[{t:3}],s:"Zapnuto"}},off:{t:0,b:{t:2,i:[{t:3}],s:"Vypnuto"}}}};export{t as default};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
input[type=range][data-v-cbc29188]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background:#6366f1;border:2px solid #1e293b;border-radius:50%;cursor:pointer;height:16px;width:16px}input[type=range][data-v-cbc29188]::-moz-range-thumb{background:#6366f1;border:2px solid #1e293b;border-radius:50%;cursor:pointer;height:16px;width:16px}.timeline-enter-active[data-v-cbc29188]{transition:all .3s ease-out}.timeline-enter-from[data-v-cbc29188]{opacity:0;transform:translate(-10px)}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.overflow-y-auto[data-v-03c94385]::-webkit-scrollbar{width:6px}.overflow-y-auto[data-v-03c94385]::-webkit-scrollbar-track{background:transparent}.overflow-y-auto[data-v-03c94385]::-webkit-scrollbar-thumb{background-color:#64748b80;border-radius:3px}.overflow-y-auto[data-v-03c94385]::-webkit-scrollbar-thumb:hover{background-color:#64748bb3}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{u as v}from"./VxZautWX.js";function h(){const o=v();function u(t,e){const r=o.activeLocale||"en-EU";try{return new Intl.NumberFormat(r,e).format(t)}catch{return new Intl.NumberFormat("en-EU",e).format(t)}}function a(t,e,r){const c=o.activeLocale||"en-EU",n=e||o.activeCurrency||"EUR";try{return new Intl.NumberFormat(c,{style:"currency",currency:n,minimumFractionDigits:2,maximumFractionDigits:2,...r}).format(t)}catch{return`${t.toFixed(2)} ${n}`}}function s(t,e){const r=o.activeLocale||"en-EU",c=o.activeTimezone||"Europe/Brussels",n=typeof t=="string"||typeof t=="number"?new Date(t):t;try{return new Intl.DateTimeFormat(r,{timezone:c,year:"numeric",month:"2-digit",day:"2-digit",...e}).format(n)}catch{return n.toLocaleDateString("en-GB")}}function l(t,e){const r=o.activeLocale||"en-EU",c=o.activeTimezone||"Europe/Brussels",n=typeof t=="string"||typeof t=="number"?new Date(t):t;try{return new Intl.DateTimeFormat(r,{timezone:c,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",...e}).format(n)}catch{return n.toLocaleString("en-GB")}}function f(t,e){const r=o.activeLocale||"en-EU";try{return new Intl.NumberFormat(r,{style:"percent",minimumFractionDigits:0,maximumFractionDigits:1,...e}).format(t/100)}catch{return`${t}%`}}function m(t,e){const r=e??o.activeVatRate??0,c=t*(r/100),n=t+c;return{net:t,vat:c,gross:n,vatRate:r}}function y(t,e,r){const{net:c,vat:n,gross:g,vatRate:F}=m(t,e),i=r||o.activeCurrency||"EUR";return`Net: ${a(c,i)} | VAT: ${a(n,i)} (${F}%) | Gross: ${a(g,i)}`}return{formatNumber:u,formatCurrency:a,formatDate:s,formatDateTime:l,formatPercent:f,calculatePriceWithVat:m,formatPriceWithVat:y}}export{h as u}; import{u as v}from"./BmPpcd_L.js";function h(){const o=v();function u(t,e){const r=o.activeLocale||"en-EU";try{return new Intl.NumberFormat(r,e).format(t)}catch{return new Intl.NumberFormat("en-EU",e).format(t)}}function a(t,e,r){const c=o.activeLocale||"en-EU",n=e||o.activeCurrency||"EUR";try{return new Intl.NumberFormat(c,{style:"currency",currency:n,minimumFractionDigits:2,maximumFractionDigits:2,...r}).format(t)}catch{return`${t.toFixed(2)} ${n}`}}function s(t,e){const r=o.activeLocale||"en-EU",c=o.activeTimezone||"Europe/Brussels",n=typeof t=="string"||typeof t=="number"?new Date(t):t;try{return new Intl.DateTimeFormat(r,{timezone:c,year:"numeric",month:"2-digit",day:"2-digit",...e}).format(n)}catch{return n.toLocaleDateString("en-GB")}}function l(t,e){const r=o.activeLocale||"en-EU",c=o.activeTimezone||"Europe/Brussels",n=typeof t=="string"||typeof t=="number"?new Date(t):t;try{return new Intl.DateTimeFormat(r,{timezone:c,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",...e}).format(n)}catch{return n.toLocaleString("en-GB")}}function f(t,e){const r=o.activeLocale||"en-EU";try{return new Intl.NumberFormat(r,{style:"percent",minimumFractionDigits:0,maximumFractionDigits:1,...e}).format(t/100)}catch{return`${t}%`}}function m(t,e){const r=e??o.activeVatRate??0,c=t*(r/100),n=t+c;return{net:t,vat:c,gross:n,vatRate:r}}function y(t,e,r){const{net:c,vat:n,gross:g,vatRate:F}=m(t,e),i=r||o.activeCurrency||"EUR";return`Net: ${a(c,i)} | VAT: ${a(n,i)} (${F}%) | Gross: ${a(g,i)}`}return{formatNumber:u,formatCurrency:a,formatDate:s,formatDateTime:l,formatPercent:f,calculatePriceWithVat:m,formatPriceWithVat:y}}export{h as u};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.animate-slide-up[data-v-c684092a]{animation:slideUp-c684092a .3s ease-out}@keyframes slideUp-c684092a{0%{opacity:0;transform:translateY(1rem)}to{opacity:1;transform:translateY(0)}}

View File

@@ -0,0 +1 @@
.animate-slide-up[data-v-9dff1238]{animation:slideUp-9dff1238 .3s ease-out}@keyframes slideUp-9dff1238{0%{opacity:0;transform:translateY(1rem)}to{opacity:1;transform:translateY(0)}}

View File

@@ -1 +0,0 @@
.animate-slide-up[data-v-c5f07ccf]{animation:slideUp-c5f07ccf .3s ease-out}@keyframes slideUp-c5f07ccf{0%{opacity:0;transform:translateY(1rem)}to{opacity:1;transform:translateY(0)}}

View File

@@ -0,0 +1 @@
.animate-slide-up[data-v-f21993c6]{animation:slideUp-f21993c6 .3s ease-out}@keyframes slideUp-f21993c6{0%{opacity:0;transform:translateY(1rem)}to{opacity:1;transform:translateY(0)}}

View File

@@ -0,0 +1 @@
.animate-slide-up[data-v-956f226d]{animation:slideUp-956f226d .3s ease-out}@keyframes slideUp-956f226d{0%{opacity:0;transform:translateY(1rem)}to{opacity:1;transform:translateY(0)}}

View File

@@ -2,7 +2,7 @@ import { defineComponent, computed, ref, unref, useSSRContext } from "vue";
import { ssrRenderAttrs, ssrInterpolate, ssrRenderClass, ssrRenderAttr, ssrIncludeBooleanAttr, ssrRenderList } from "vue/server-renderer"; import { ssrRenderAttrs, ssrInterpolate, ssrRenderClass, ssrRenderAttr, ssrIncludeBooleanAttr, ssrRenderList } from "vue/server-renderer";
import "/app/node_modules/hookable/dist/index.mjs"; import "/app/node_modules/hookable/dist/index.mjs";
import { u as useFormatter } from "./useFormatter-hycy3urM.js"; import { u as useFormatter } from "./useFormatter-hycy3urM.js";
import { a as useI18n, b as useRouter, e as useRoute } from "../server.mjs"; import { a as useI18n, b as useRouter, i as useRoute } from "../server.mjs";
import "/app/node_modules/klona/dist/index.mjs"; import "/app/node_modules/klona/dist/index.mjs";
import "./region-0mvXqaMM.js"; import "./region-0mvXqaMM.js";
import "pinia"; import "pinia";
@@ -128,4 +128,4 @@ _sfc_main.setup = (props, ctx) => {
export { export {
_sfc_main as default _sfc_main as default
}; };
//# sourceMappingURL=_id_-CoXhCsuZ.js.map //# sourceMappingURL=_id_-DvpH9xyV.js.map

View File

@@ -0,0 +1,466 @@
import { _ as __nuxt_component_0 } from "./nuxt-link-1NBbHb-h.js";
import { defineComponent, ref, computed, watch, createVNode, resolveDynamicComponent, withCtx, openBlock, createBlock, createTextVNode, useSSRContext } from "vue";
import { ssrRenderAttrs, ssrInterpolate, ssrRenderClass, ssrRenderList, ssrRenderVNode, ssrRenderStyle, ssrRenderAttr, ssrIncludeBooleanAttr, ssrRenderComponent, ssrLooseContain, ssrLooseEqual } from "vue/server-renderer";
import { useRoute, useRouter } from "vue-router";
import { d as useCookie } from "../server.mjs";
import { _ as _export_sfc } from "./_plugin-vue_export-helper-1tPrXgE0.js";
import "/app/node_modules/ufo/dist/index.mjs";
import "/app/node_modules/defu/dist/defu.mjs";
import "/app/node_modules/ofetch/dist/node.mjs";
import "#internal/nuxt/paths";
import "/app/node_modules/hookable/dist/index.mjs";
import "/app/node_modules/unctx/dist/index.mjs";
import "/app/node_modules/h3/dist/index.mjs";
import "pinia";
import "/app/node_modules/klona/dist/index.mjs";
import "/app/node_modules/cookie-es/dist/index.mjs";
import "/app/node_modules/destr/dist/index.mjs";
import "/app/node_modules/ohash/dist/index.mjs";
import "/app/node_modules/@unhead/vue/dist/index.mjs";
import "@vue/devtools-api";
const _sfc_main = /* @__PURE__ */ defineComponent({
__name: "[id]",
__ssrInlineRender: true,
setup(__props) {
const route = useRoute();
useRouter();
const loading = ref(true);
const error = ref(false);
const provider = ref(null);
const actionLoading = ref(false);
ref(null);
const showRejectModal = ref(false);
const rejectReason = ref("");
const showFlagModal = ref(false);
const flagReason = ref("");
const showEditModal = ref(false);
const editLoading = ref(false);
const editForm = ref({});
const activeTab = ref("data");
const validations = ref([]);
const validationsLoading = ref(false);
const history = ref([]);
const historyLoading = ref(false);
const statusForm = ref({
status: "pending",
validation_score: 0,
reason: ""
});
const statusLoading = ref(false);
const tabs = [
{ key: "data", label: "Adatok", icon: "svg" },
{ key: "status", label: "Státusz", icon: "svg" },
{ key: "validations", label: "Validációk", icon: "svg" },
{ key: "history", label: "Előzmények", icon: "svg" }
];
const statusBannerClass = computed(() => {
switch (provider.value?.status) {
case "approved":
return "bg-emerald-500/10 border-emerald-500/30";
case "pending":
return "bg-amber-500/10 border-amber-500/30";
default:
return "bg-rose-500/10 border-rose-500/30";
}
});
const statusIconClass = computed(() => {
switch (provider.value?.status) {
case "approved":
return "bg-emerald-500/20 text-emerald-400";
case "pending":
return "bg-amber-500/20 text-amber-400";
default:
return "bg-rose-500/20 text-rose-400";
}
});
const statusTextClass = computed(() => {
switch (provider.value?.status) {
case "approved":
return "text-emerald-400";
case "pending":
return "text-amber-400";
default:
return "text-rose-400";
}
});
const statusSubtextClass = computed(() => {
switch (provider.value?.status) {
case "approved":
return "text-emerald-400/60";
case "pending":
return "text-amber-400/60";
default:
return "text-rose-400/60";
}
});
function statusLabel(status) {
switch (status) {
case "approved":
return "Jóváhagyva";
case "pending":
return "Függőben";
case "rejected":
return "Elutasítva";
default:
return "Ismeretlen";
}
}
function statusDescription(status) {
switch (status) {
case "approved":
return "A szolgáltató aktív és látható a felhasználók számára.";
case "pending":
return "A szolgáltató még nem lett jóváhagyva, moderálásra vár.";
case "rejected":
return "A szolgáltató elutasításra került, nem látható a felhasználók számára.";
default:
return "";
}
}
function validationTypeLabel(type) {
const labels = {
phone_verified: "Telefon igazolt",
email_verified: "Email igazolt",
address_verified: "Cím igazolt",
website_verified: "Weboldal igazolt",
manual_review: "Kézi ellenőrzés",
community_vouch: "Közösségi ajánlás",
business_hours: "Nyitvatartás ellenőrzött",
photo_evidence: "Fénykép bizonyíték"
};
return labels[type] || type;
}
function validationTypeClass(type) {
const classes = {
phone_verified: "bg-emerald-500/20 text-emerald-400",
email_verified: "bg-blue-500/20 text-blue-400",
address_verified: "bg-amber-500/20 text-amber-400",
website_verified: "bg-purple-500/20 text-purple-400",
manual_review: "bg-indigo-500/20 text-indigo-400",
community_vouch: "bg-pink-500/20 text-pink-400",
business_hours: "bg-cyan-500/20 text-cyan-400",
photo_evidence: "bg-orange-500/20 text-orange-400"
};
return classes[type] || "bg-slate-500/20 text-slate-400";
}
function historyActionLabel(action) {
const labels = {
approve: "Jóváhagyás",
reject: "Elutasítás",
restore: "Visszaállítás",
flag: "Megjelölés",
update: "Módosítás",
delete: "Törlés",
create: "Létrehozás"
};
return labels[action] || action;
}
function historyActionClass(action) {
const classes = {
approve: "bg-emerald-500/20 text-emerald-400",
reject: "bg-rose-500/20 text-rose-400",
restore: "bg-blue-500/20 text-blue-400",
flag: "bg-purple-500/20 text-purple-400",
update: "bg-amber-500/20 text-amber-400",
delete: "bg-red-500/20 text-red-400",
create: "bg-green-500/20 text-green-400"
};
return classes[action] || "bg-slate-500/20 text-slate-400";
}
function historyDotClass(action) {
const classes = {
approve: "border-emerald-500 bg-emerald-500/30",
reject: "border-rose-500 bg-rose-500/30",
restore: "border-blue-500 bg-blue-500/30",
flag: "border-purple-500 bg-purple-500/30",
update: "border-amber-500 bg-amber-500/30",
delete: "border-red-500 bg-red-500/30",
create: "border-green-500 bg-green-500/30"
};
return classes[action] || "border-slate-500 bg-slate-500/30";
}
function formatDate(dateStr) {
if (!dateStr) return "—";
try {
const d = new Date(dateStr);
return d.toLocaleDateString("hu-HU", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
});
} catch {
return dateStr;
}
}
function getHeaders() {
const token = useCookie("access_token").value;
const headers = { "Content-Type": "application/json" };
if (token) headers["Authorization"] = `Bearer ${token}`;
return headers;
}
async function fetchValidations() {
validationsLoading.value = true;
try {
const res = await $fetch(`/api/v1/admin/providers/${route.params.id}/validations`, {
headers: getHeaders()
});
validations.value = res || [];
} catch (err) {
console.error("Failed to fetch validations:", err);
validations.value = [];
} finally {
validationsLoading.value = false;
}
}
async function fetchHistory() {
historyLoading.value = true;
try {
const res = await $fetch(`/api/v1/admin/providers/${route.params.id}/history`, {
headers: getHeaders()
});
history.value = res || [];
} catch (err) {
console.error("Failed to fetch history:", err);
history.value = [];
} finally {
historyLoading.value = false;
}
}
function onTabChange(tab) {
if (tab === "validations" && validations.value.length === 0) {
fetchValidations();
}
if (tab === "history" && history.value.length === 0) {
fetchHistory();
}
}
watch(activeTab, onTabChange);
return (_ctx, _push, _parent, _attrs) => {
const _component_NuxtLink = __nuxt_component_0;
_push(`<div${ssrRenderAttrs(_attrs)} data-v-cbc29188><div class="mb-8" data-v-cbc29188><button class="inline-flex items-center gap-1.5 text-sm text-slate-400 hover:text-white transition mb-4" data-v-cbc29188><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" data-v-cbc29188></path></svg> Vissza a szolgáltatókhoz </button><h1 class="text-2xl font-bold text-white" data-v-cbc29188>Szolgáltató részletek</h1><p class="text-slate-400 mt-1" data-v-cbc29188>Szolgáltató #${ssrInterpolate(_ctx.$route.params.id)}</p></div>`);
if (loading.value) {
_push(`<div class="flex items-center justify-center py-20" data-v-cbc29188><div class="text-slate-400 flex items-center gap-3" data-v-cbc29188><svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><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" data-v-cbc29188></path></svg><span data-v-cbc29188>Szolgáltató betöltése...</span></div></div>`);
} else if (error.value) {
_push(`<div class="bg-rose-500/10 border border-rose-500/30 rounded-xl p-6 mb-8" data-v-cbc29188><p class="text-rose-400" data-v-cbc29188>Hiba történt a szolgáltató betöltése közben.</p><button class="mt-3 text-sm text-rose-300 hover:text-rose-200 underline" data-v-cbc29188>Újrapróbálkozás</button></div>`);
} else if (!provider.value) {
_push(`<div class="bg-slate-800 rounded-xl border border-slate-700 p-6" data-v-cbc29188><p class="text-slate-400 text-center py-12" data-v-cbc29188>A szolgáltató nem található.</p></div>`);
} else {
_push(`<!--[--><div class="${ssrRenderClass([statusBannerClass.value, "rounded-xl border p-4 mb-6 flex items-center gap-3"])}" data-v-cbc29188><span class="${ssrRenderClass([statusIconClass.value, "flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center"])}" data-v-cbc29188>`);
if (provider.value.status === "approved") {
_push(`<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" data-v-cbc29188></path></svg>`);
} else if (provider.value.status === "pending") {
_push(`<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" data-v-cbc29188></path></svg>`);
} else {
_push(`<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" data-v-cbc29188></path></svg>`);
}
_push(`</span><div data-v-cbc29188><p class="${ssrRenderClass([statusTextClass.value, "text-sm font-medium"])}" data-v-cbc29188>${ssrInterpolate(statusLabel(provider.value.status))}</p><p class="${ssrRenderClass([statusSubtextClass.value, "text-xs mt-0.5"])}" data-v-cbc29188>${ssrInterpolate(statusDescription(provider.value.status))}</p></div></div><div class="border-b border-slate-700 mb-6" data-v-cbc29188><nav class="flex gap-6 -mb-px" data-v-cbc29188><!--[-->`);
ssrRenderList(tabs, (tab) => {
_push(`<button class="${ssrRenderClass([activeTab.value === tab.key ? "text-indigo-400 border-indigo-400" : "text-slate-400 border-transparent hover:text-slate-300", "pb-3 text-sm font-medium border-b-2 transition flex items-center gap-2"])}" data-v-cbc29188>`);
ssrRenderVNode(_push, createVNode(resolveDynamicComponent(tab.icon), { class: "w-4 h-4" }, null), _parent);
_push(` ${ssrInterpolate(tab.label)}</button>`);
});
_push(`<!--]--></nav></div>`);
if (activeTab.value === "data") {
_push(`<div class="grid grid-cols-1 lg:grid-cols-3 gap-6" data-v-cbc29188><div class="lg:col-span-2 space-y-6" data-v-cbc29188><div class="bg-slate-800 rounded-xl border border-slate-700 p-6" data-v-cbc29188><h2 class="text-lg font-semibold text-white mb-4" data-v-cbc29188>Alapadatok</h2><dl class="grid grid-cols-1 md:grid-cols-2 gap-4" data-v-cbc29188><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Név</dt><dd class="text-sm text-white mt-1 font-medium" data-v-cbc29188>${ssrInterpolate(provider.value.name)}</dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Kategória</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>${ssrInterpolate(provider.value.category || "—")}</dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Forrás</dt><dd class="mt-1" data-v-cbc29188><code class="text-sm font-mono text-amber-300 bg-amber-500/10 px-2 py-0.5 rounded" data-v-cbc29188>${ssrInterpolate(provider.value.source)}</code></dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Validációs pontszám</dt><dd class="mt-1" data-v-cbc29188><div class="flex items-center gap-2" data-v-cbc29188><div class="w-24 h-2 bg-slate-700 rounded-full overflow-hidden" data-v-cbc29188><div class="${ssrRenderClass([provider.value.validation_score >= 50 ? "bg-emerald-500" : provider.value.validation_score >= 20 ? "bg-amber-500" : "bg-slate-500", "h-full rounded-full transition-all"])}" style="${ssrRenderStyle({ width: Math.min(provider.value.validation_score, 100) + "%" })}" data-v-cbc29188></div></div><span class="text-sm text-white font-medium" data-v-cbc29188>${ssrInterpolate(provider.value.validation_score)}</span></div></dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Létrehozva</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>${ssrInterpolate(formatDate(provider.value.created_at))}</dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Beküldő user ID</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>${ssrInterpolate(provider.value.added_by_user_id || "—")}</dd></div></dl></div><div class="bg-slate-800 rounded-xl border border-slate-700 p-6" data-v-cbc29188><h2 class="text-lg font-semibold text-white mb-4" data-v-cbc29188>Cím adatok</h2><dl class="grid grid-cols-1 md:grid-cols-2 gap-4" data-v-cbc29188><div class="md:col-span-2" data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Teljes cím</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>${ssrInterpolate(provider.value.address || "—")}</dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Város</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>${ssrInterpolate(provider.value.city || "—")}</dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Irányítószám</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>${ssrInterpolate(provider.value.address_zip || "—")}</dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Utca</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>${ssrInterpolate(provider.value.address_street_name || "—")}</dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Házszám</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>${ssrInterpolate(provider.value.address_house_number || "—")}</dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Plus Code</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>${ssrInterpolate(provider.value.plus_code || "—")}</dd></div></dl></div><div class="bg-slate-800 rounded-xl border border-slate-700 p-6" data-v-cbc29188><h2 class="text-lg font-semibold text-white mb-4" data-v-cbc29188>Elérhetőség</h2><dl class="grid grid-cols-1 md:grid-cols-2 gap-4" data-v-cbc29188><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Telefon</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>${ssrInterpolate(provider.value.contact_phone || "—")}</dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Email</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>${ssrInterpolate(provider.value.contact_email || "—")}</dd></div><div data-v-cbc29188><dt class="text-xs text-slate-500 uppercase tracking-wider" data-v-cbc29188>Weboldal</dt><dd class="text-sm text-white mt-1" data-v-cbc29188>`);
if (provider.value.website) {
_push(`<a${ssrRenderAttr("href", provider.value.website)} target="_blank" class="text-indigo-400 hover:text-indigo-300 underline" data-v-cbc29188>${ssrInterpolate(provider.value.website)}</a>`);
} else {
_push(`<span data-v-cbc29188>—</span>`);
}
_push(`</dd></div></dl></div></div><div class="space-y-4" data-v-cbc29188><div class="bg-slate-800 rounded-xl border border-slate-700 p-6" data-v-cbc29188><h2 class="text-lg font-semibold text-white mb-4" data-v-cbc29188>Moderációs műveletek</h2>`);
if (provider.value.status === "pending") {
_push(`<div class="space-y-3" data-v-cbc29188><button${ssrIncludeBooleanAttr(actionLoading.value) ? " disabled" : ""} class="w-full px-4 py-3 bg-emerald-600 hover:bg-emerald-500 disabled:bg-emerald-600/50 text-white rounded-lg text-sm font-medium transition flex items-center justify-center gap-2" data-v-cbc29188>`);
if (actionLoading.value) {
_push(`<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><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" data-v-cbc29188></path></svg>`);
} else {
_push(`<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" data-v-cbc29188></path></svg>`);
}
_push(` Jóváhagyás </button><button${ssrIncludeBooleanAttr(actionLoading.value) ? " disabled" : ""} class="w-full px-4 py-3 bg-rose-600/20 hover:bg-rose-600/40 disabled:opacity-50 text-rose-400 rounded-lg text-sm font-medium transition flex items-center justify-center gap-2" data-v-cbc29188><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" data-v-cbc29188></path></svg> Elutasítás </button></div>`);
} else if (provider.value.status === "approved") {
_push(`<div class="space-y-3" data-v-cbc29188>`);
_push(ssrRenderComponent(_component_NuxtLink, {
to: `/providers/${provider.value.id}/edit`,
class: "w-full px-4 py-3 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-sm font-medium transition flex items-center justify-center gap-2"
}, {
default: withCtx((_, _push2, _parent2, _scopeId) => {
if (_push2) {
_push2(`<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188${_scopeId}><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" data-v-cbc29188${_scopeId}></path></svg> Szerkesztés `);
} else {
return [
(openBlock(), createBlock("svg", {
class: "w-4 h-4",
fill: "none",
stroke: "currentColor",
viewBox: "0 0 24 24"
}, [
createVNode("path", {
"stroke-linecap": "round",
"stroke-linejoin": "round",
"stroke-width": "2",
d: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
})
])),
createTextVNode(" Szerkesztés ")
];
}
}),
_: 1
}, _parent));
_push(`<button${ssrIncludeBooleanAttr(actionLoading.value) ? " disabled" : ""} class="w-full px-4 py-3 bg-purple-600/20 hover:bg-purple-600/40 disabled:opacity-50 text-purple-400 rounded-lg text-sm font-medium transition flex items-center justify-center gap-2" data-v-cbc29188><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" data-v-cbc29188></path></svg> Megjelölés (Flag) </button><button${ssrIncludeBooleanAttr(actionLoading.value) ? " disabled" : ""} class="w-full px-4 py-3 bg-rose-600/20 hover:bg-rose-600/40 disabled:opacity-50 text-rose-400 rounded-lg text-sm font-medium transition flex items-center justify-center gap-2" data-v-cbc29188><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" data-v-cbc29188></path></svg> Törlés (Soft Delete) </button></div>`);
} else {
_push(`<div class="space-y-3" data-v-cbc29188><p class="text-sm text-slate-400 text-center py-2" data-v-cbc29188> A szolgáltató ${ssrInterpolate(provider.value.status === "rejected" ? "elutasításra" : "megjelölésre")} került. </p><button${ssrIncludeBooleanAttr(actionLoading.value) ? " disabled" : ""} class="w-full px-4 py-3 bg-indigo-600 hover:bg-indigo-500 disabled:bg-indigo-600/50 text-white rounded-lg text-sm font-medium transition flex items-center justify-center gap-2" data-v-cbc29188><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" data-v-cbc29188></path></svg> Szerkesztés </button>`);
if (provider.value.status === "rejected") {
_push(`<button${ssrIncludeBooleanAttr(actionLoading.value) ? " disabled" : ""} class="w-full px-4 py-3 bg-emerald-600 hover:bg-emerald-500 disabled:bg-emerald-600/50 text-white rounded-lg text-sm font-medium transition flex items-center justify-center gap-2" data-v-cbc29188><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><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" data-v-cbc29188></path></svg> Visszaállítás </button>`);
} else {
_push(`<!---->`);
}
_push(`<button${ssrIncludeBooleanAttr(actionLoading.value) ? " disabled" : ""} class="w-full px-4 py-3 bg-rose-600/20 hover:bg-rose-600/40 disabled:opacity-50 text-rose-400 rounded-lg text-sm font-medium transition flex items-center justify-center gap-2" data-v-cbc29188><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" data-v-cbc29188></path></svg> Törlés </button></div>`);
}
_push(`</div>`);
if (provider.value.evidence_image_path) {
_push(`<div class="bg-slate-800 rounded-xl border border-slate-700 p-6" data-v-cbc29188><h2 class="text-lg font-semibold text-white mb-4" data-v-cbc29188>Bizonyíték kép</h2><img${ssrRenderAttr("src", provider.value.evidence_image_path)} alt="Evidence" class="w-full rounded-lg border border-slate-600" data-v-cbc29188></div>`);
} else {
_push(`<!---->`);
}
_push(`</div></div>`);
} else {
_push(`<!---->`);
}
if (activeTab.value === "status") {
_push(`<div class="max-w-2xl" data-v-cbc29188><div class="bg-slate-800 rounded-xl border border-slate-700 p-6" data-v-cbc29188><h2 class="text-lg font-semibold text-white mb-4" data-v-cbc29188>Státusz kezelés</h2><p class="text-sm text-slate-400 mb-6" data-v-cbc29188>Módosítsd a szolgáltató státuszát és validációs pontszámát.</p><form data-v-cbc29188><div class="space-y-4 mb-6" data-v-cbc29188><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Státusz</label><select 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-indigo-500/50 focus:border-indigo-500 text-sm" data-v-cbc29188><option value="pending" data-v-cbc29188${ssrIncludeBooleanAttr(Array.isArray(statusForm.value.status) ? ssrLooseContain(statusForm.value.status, "pending") : ssrLooseEqual(statusForm.value.status, "pending")) ? " selected" : ""}>Függőben</option><option value="approved" data-v-cbc29188${ssrIncludeBooleanAttr(Array.isArray(statusForm.value.status) ? ssrLooseContain(statusForm.value.status, "approved") : ssrLooseEqual(statusForm.value.status, "approved")) ? " selected" : ""}>Jóváhagyott</option><option value="rejected" data-v-cbc29188${ssrIncludeBooleanAttr(Array.isArray(statusForm.value.status) ? ssrLooseContain(statusForm.value.status, "rejected") : ssrLooseEqual(statusForm.value.status, "rejected")) ? " selected" : ""}>Elutasított</option></select></div><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188> Validációs pontszám (0-100) </label><div class="flex items-center gap-3" data-v-cbc29188><input${ssrRenderAttr("value", statusForm.value.validation_score)} type="range" min="0" max="100" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-indigo-500" data-v-cbc29188><span class="text-sm text-white font-medium w-8 text-right" data-v-cbc29188>${ssrInterpolate(statusForm.value.validation_score)}</span></div></div><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Indoklás (opcionális)</label><textarea rows="3" placeholder="Add meg a státuszváltoztatás okát..." 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-indigo-500/50 focus:border-indigo-500 text-sm resize-none" data-v-cbc29188>${ssrInterpolate(statusForm.value.reason)}</textarea></div></div><div class="flex justify-end gap-3" data-v-cbc29188><button type="button" class="px-4 py-2 text-sm text-slate-400 hover:text-white transition" data-v-cbc29188>Mégsem</button><button type="submit"${ssrIncludeBooleanAttr(statusLoading.value) ? " disabled" : ""} class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:bg-indigo-600/50 text-white rounded-lg text-sm font-medium transition flex items-center gap-2" data-v-cbc29188>`);
if (statusLoading.value) {
_push(`<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><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" data-v-cbc29188></path></svg>`);
} else {
_push(`<!---->`);
}
_push(` Státusz mentése </button></div></form></div></div>`);
} else {
_push(`<!---->`);
}
if (activeTab.value === "validations") {
_push(`<div data-v-cbc29188><div class="bg-slate-800 rounded-xl border border-slate-700 p-6" data-v-cbc29188><h2 class="text-lg font-semibold text-white mb-4" data-v-cbc29188>Gamification Validációk</h2><p class="text-sm text-slate-400 mb-6" data-v-cbc29188>A szolgáltatóhoz tartozó gamification validációs bejegyzések listája.</p>`);
if (validationsLoading.value) {
_push(`<div class="flex items-center justify-center py-8" data-v-cbc29188><svg class="w-5 h-5 animate-spin text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><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" data-v-cbc29188></path></svg></div>`);
} else if (validations.value.length === 0) {
_push(`<div class="text-center py-8" data-v-cbc29188><p class="text-slate-500" data-v-cbc29188>Nincsenek validációs bejegyzések ehhez a szolgáltatóhoz.</p></div>`);
} else {
_push(`<div class="space-y-3" data-v-cbc29188><!--[-->`);
ssrRenderList(validations.value, (v) => {
_push(`<div class="bg-slate-700/50 rounded-lg border border-slate-600 p-4" data-v-cbc29188><div class="flex items-start justify-between gap-4" data-v-cbc29188><div class="flex-1" data-v-cbc29188><div class="flex items-center gap-2 mb-1" data-v-cbc29188><span class="${ssrRenderClass([validationTypeClass(v.validation_type), "text-xs font-medium px-2 py-0.5 rounded-full"])}" data-v-cbc29188>${ssrInterpolate(validationTypeLabel(v.validation_type))}</span><span class="text-xs text-slate-500" data-v-cbc29188>súly: ${ssrInterpolate(v.weight)}</span></div><p class="text-sm text-slate-300" data-v-cbc29188><span class="text-slate-400" data-v-cbc29188>Szavazó:</span> ${ssrInterpolate(v.voter_name || `User #${v.voter_user_id}`)}</p>`);
if (v.validation_metadata?.reason) {
_push(`<p class="text-xs text-slate-500 mt-1" data-v-cbc29188> Indoklás: ${ssrInterpolate(v.validation_metadata.reason)}</p>`);
} else {
_push(`<!---->`);
}
_push(`</div><div class="text-xs text-slate-500 whitespace-nowrap" data-v-cbc29188>${ssrInterpolate(formatDate(v.created_at))}</div></div></div>`);
});
_push(`<!--]--></div>`);
}
_push(`</div></div>`);
} else {
_push(`<!---->`);
}
if (activeTab.value === "history") {
_push(`<div data-v-cbc29188><div class="bg-slate-800 rounded-xl border border-slate-700 p-6" data-v-cbc29188><h2 class="text-lg font-semibold text-white mb-4" data-v-cbc29188>Állapotváltoztatások előzményei</h2><p class="text-sm text-slate-400 mb-6" data-v-cbc29188>Idővonal: ki, mikor és mit változtatott a szolgáltatóval kapcsolatban.</p>`);
if (historyLoading.value) {
_push(`<div class="flex items-center justify-center py-8" data-v-cbc29188><svg class="w-5 h-5 animate-spin text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><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" data-v-cbc29188></path></svg></div>`);
} else if (history.value.length === 0) {
_push(`<div class="text-center py-8" data-v-cbc29188><p class="text-slate-500" data-v-cbc29188>Nincsenek előzmények ehhez a szolgáltatóhoz.</p></div>`);
} else {
_push(`<div class="relative" data-v-cbc29188><div class="absolute left-4 top-0 bottom-0 w-0.5 bg-slate-700" data-v-cbc29188></div><!--[-->`);
ssrRenderList(history.value, (entry, index) => {
_push(`<div class="relative pl-10 pb-6 last:pb-0" data-v-cbc29188><div class="${ssrRenderClass([historyDotClass(entry.action), "absolute left-2.5 w-3 h-3 rounded-full border-2 mt-1.5"])}" data-v-cbc29188></div><div class="bg-slate-700/50 rounded-lg border border-slate-600 p-4" data-v-cbc29188><div class="flex items-start justify-between gap-4 mb-2" data-v-cbc29188><div data-v-cbc29188><span class="${ssrRenderClass([historyActionClass(entry.action), "text-xs font-medium px-2 py-0.5 rounded-full"])}" data-v-cbc29188>${ssrInterpolate(historyActionLabel(entry.action))}</span><span class="text-xs text-slate-500 ml-2" data-v-cbc29188> User #${ssrInterpolate(entry.user_id || "?")}</span></div><div class="text-xs text-slate-500 whitespace-nowrap" data-v-cbc29188>${ssrInterpolate(formatDate(entry.timestamp))}</div></div>`);
if (entry.new_data) {
_push(`<div class="text-xs text-slate-400 space-y-1 mt-2" data-v-cbc29188>`);
if (entry.new_data.old_status || entry.new_data.new_status) {
_push(`<div class="flex items-center gap-2" data-v-cbc29188><span class="text-slate-500" data-v-cbc29188>Státusz:</span><span class="text-rose-400" data-v-cbc29188>${ssrInterpolate(entry.new_data.old_status || "?")}</span><svg class="w-3 h-3 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" data-v-cbc29188></path></svg><span class="text-emerald-400" data-v-cbc29188>${ssrInterpolate(entry.new_data.new_status || "?")}</span></div>`);
} else {
_push(`<!---->`);
}
if (entry.new_data.reason && entry.new_data.reason !== "Admin edit") {
_push(`<div data-v-cbc29188><span class="text-slate-500" data-v-cbc29188>Indoklás:</span> ${ssrInterpolate(entry.new_data.reason)}</div>`);
} else {
_push(`<!---->`);
}
if (entry.new_data.changes) {
_push(`<div class="mt-2" data-v-cbc29188><details class="text-xs" data-v-cbc29188><summary class="text-slate-500 cursor-pointer hover:text-slate-400" data-v-cbc29188>Részletes változások</summary><div class="mt-2 space-y-1" data-v-cbc29188><!--[-->`);
ssrRenderList(entry.new_data.changes, (value, key) => {
_push(`<div class="flex gap-2" data-v-cbc29188><span class="text-slate-500 w-28 flex-shrink-0" data-v-cbc29188>${ssrInterpolate(key)}:</span><span class="text-slate-300 truncate" data-v-cbc29188>${ssrInterpolate(value ?? "—")}</span></div>`);
});
_push(`<!--]--></div></details></div>`);
} else {
_push(`<!---->`);
}
_push(`</div>`);
} else {
_push(`<!---->`);
}
if (entry.old_data) {
_push(`<div class="mt-2" data-v-cbc29188><details class="text-xs" data-v-cbc29188><summary class="text-slate-500 cursor-pointer hover:text-slate-400" data-v-cbc29188>Régi adatok</summary><div class="mt-2 space-y-1" data-v-cbc29188><!--[-->`);
ssrRenderList(entry.old_data, (value, key) => {
_push(`<div class="flex gap-2" data-v-cbc29188><span class="text-slate-500 w-28 flex-shrink-0" data-v-cbc29188>${ssrInterpolate(key)}:</span><span class="text-slate-300 truncate" data-v-cbc29188>${ssrInterpolate(value ?? "—")}</span></div>`);
});
_push(`<!--]--></div></details></div>`);
} else {
_push(`<!---->`);
}
_push(`</div></div>`);
});
_push(`<!--]--></div>`);
}
_push(`</div></div>`);
} else {
_push(`<!---->`);
}
_push(`<!--]-->`);
}
if (showRejectModal.value) {
_push(`<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" data-v-cbc29188><div class="bg-slate-800 rounded-xl border border-slate-700 p-6 w-full max-w-lg mx-4 shadow-2xl" data-v-cbc29188><h2 class="text-lg font-semibold text-white mb-2" data-v-cbc29188>Szolgáltató elutasítása</h2><p class="text-sm text-slate-400 mb-6" data-v-cbc29188> Biztosan elutasítod a(z) <strong class="text-white" data-v-cbc29188>${ssrInterpolate(provider.value?.name)}</strong> szolgáltatót? </p><form data-v-cbc29188><div class="mb-4" data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Indoklás (opcionális)</label><textarea rows="3" placeholder="Add meg az elutasítás okát..." 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-rose-500/50 focus:border-rose-500 text-sm resize-none" data-v-cbc29188>${ssrInterpolate(rejectReason.value)}</textarea></div><div class="flex justify-end gap-3" data-v-cbc29188><button type="button" class="px-4 py-2 text-sm text-slate-400 hover:text-white transition" data-v-cbc29188>Mégse</button><button type="submit"${ssrIncludeBooleanAttr(actionLoading.value) ? " disabled" : ""} class="px-4 py-2 bg-rose-600 hover:bg-rose-500 disabled:bg-rose-600/50 text-white rounded-lg text-sm font-medium transition flex items-center gap-2" data-v-cbc29188>`);
if (actionLoading.value) {
_push(`<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><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" data-v-cbc29188></path></svg>`);
} else {
_push(`<!---->`);
}
_push(` Elutasítás </button></div></form></div></div>`);
} else {
_push(`<!---->`);
}
if (showEditModal.value) {
_push(`<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" data-v-cbc29188><div class="bg-slate-800 rounded-xl border border-slate-700 p-6 w-full max-w-lg mx-4 shadow-2xl max-h-[90vh] overflow-y-auto" data-v-cbc29188><h2 class="text-lg font-semibold text-white mb-2" data-v-cbc29188>Szolgáltató szerkesztése</h2><p class="text-sm text-slate-400 mb-6" data-v-cbc29188> Módosítsd a(z) <strong class="text-white" data-v-cbc29188>${ssrInterpolate(provider.value?.name)}</strong> szolgáltató adatait. </p><form data-v-cbc29188><div class="space-y-4 mb-6" data-v-cbc29188><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Név</label><input${ssrRenderAttr("value", editForm.value.name)} 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-indigo-500/50 focus:border-indigo-500 text-sm" data-v-cbc29188></div><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Város</label><input${ssrRenderAttr("value", editForm.value.city)} 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-indigo-500/50 focus:border-indigo-500 text-sm" data-v-cbc29188></div><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Irányítószám</label><input${ssrRenderAttr("value", editForm.value.address_zip)} 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-indigo-500/50 focus:border-indigo-500 text-sm" data-v-cbc29188></div><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Utca</label><input${ssrRenderAttr("value", editForm.value.address_street_name)} 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-indigo-500/50 focus:border-indigo-500 text-sm" data-v-cbc29188></div><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Házszám</label><input${ssrRenderAttr("value", editForm.value.address_house_number)} 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-indigo-500/50 focus:border-indigo-500 text-sm" data-v-cbc29188></div><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Plus Code</label><input${ssrRenderAttr("value", editForm.value.plus_code)} 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-indigo-500/50 focus:border-indigo-500 text-sm" data-v-cbc29188></div><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Telefon</label><input${ssrRenderAttr("value", editForm.value.contact_phone)} 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-indigo-500/50 focus:border-indigo-500 text-sm" data-v-cbc29188></div><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Email</label><input${ssrRenderAttr("value", editForm.value.contact_email)} type="email" 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-indigo-500/50 focus:border-indigo-500 text-sm" data-v-cbc29188></div><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Weboldal</label><input${ssrRenderAttr("value", editForm.value.website)} 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-indigo-500/50 focus:border-indigo-500 text-sm" data-v-cbc29188></div><div data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Kategória</label><input${ssrRenderAttr("value", editForm.value.category)} 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-indigo-500/50 focus:border-indigo-500 text-sm" data-v-cbc29188></div></div><div class="flex justify-end gap-3" data-v-cbc29188><button type="button" class="px-4 py-2 text-sm text-slate-400 hover:text-white transition" data-v-cbc29188>Mégse</button><button type="submit"${ssrIncludeBooleanAttr(editLoading.value) ? " disabled" : ""} class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:bg-indigo-600/50 text-white rounded-lg text-sm font-medium transition flex items-center gap-2" data-v-cbc29188>`);
if (editLoading.value) {
_push(`<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><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" data-v-cbc29188></path></svg>`);
} else {
_push(`<!---->`);
}
_push(` Mentés </button></div></form></div></div>`);
} else {
_push(`<!---->`);
}
if (showFlagModal.value) {
_push(`<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" data-v-cbc29188><div class="bg-slate-800 rounded-xl border border-slate-700 p-6 w-full max-w-lg mx-4 shadow-2xl" data-v-cbc29188><h2 class="text-lg font-semibold text-white mb-2" data-v-cbc29188>Szolgáltató megjelölése</h2><p class="text-sm text-slate-400 mb-6" data-v-cbc29188> Biztosan megjelölöd a(z) <strong class="text-white" data-v-cbc29188>${ssrInterpolate(provider.value?.name)}</strong> szolgáltatót? </p><form data-v-cbc29188><div class="mb-4" data-v-cbc29188><label class="block text-sm font-medium text-slate-300 mb-1" data-v-cbc29188>Indoklás (opcionális)</label><textarea rows="3" placeholder="Add meg a megjelölés okát..." 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-purple-500/50 focus:border-purple-500 text-sm resize-none" data-v-cbc29188>${ssrInterpolate(flagReason.value)}</textarea></div><div class="flex justify-end gap-3" data-v-cbc29188><button type="button" class="px-4 py-2 text-sm text-slate-400 hover:text-white transition" data-v-cbc29188>Mégse</button><button type="submit"${ssrIncludeBooleanAttr(actionLoading.value) ? " disabled" : ""} class="px-4 py-2 bg-purple-600 hover:bg-purple-500 disabled:bg-purple-600/50 text-white rounded-lg text-sm font-medium transition flex items-center gap-2" data-v-cbc29188>`);
if (actionLoading.value) {
_push(`<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-v-cbc29188><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" data-v-cbc29188></path></svg>`);
} else {
_push(`<!---->`);
}
_push(` Megjelölés </button></div></form></div></div>`);
} else {
_push(`<!---->`);
}
_push(`</div>`);
};
}
});
const _sfc_setup = _sfc_main.setup;
_sfc_main.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("pages/providers/[id].vue");
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
};
const _id_ = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-cbc29188"]]);
export {
_id_ as default
};
//# sourceMappingURL=_id_-YGQP6BWl.js.map

Some files were not shown because too many files have changed in this diff Show More