Compare commits
3 Commits
1e6f79ca22
...
df4a0f07ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df4a0f07ff | ||
|
|
383e5511b6 | ||
|
|
f2935cbd64 |
269
.roo/history.md
269
.roo/history.md
@@ -823,3 +823,272 @@ Detailed fix spec written to `docs/admin_garage_subscription_tab_bugfix_spec.md`
|
||||
- `sync_engine.py`: 1278 elements OK — system perfectly in sync
|
||||
- `test_subscription_feature_flags.py`: All 6 steps passed (login, feature flags, check, public tiers)
|
||||
- Python syntax check: ✅ Syntax OK
|
||||
|
||||
## 2026-06-27 - P0 CORE ARCHITECTURE: Multi-Subscription Aggregation (Base + Add-ons)
|
||||
|
||||
### 🎯 Cél
|
||||
"1 Base Tier + N Add-on Tiers" modell implementálása. A rendszer most már több aktív előfizetést kezel szervezetenként, és aggregálja a korlátokat (asset_limit, branch_limit, user_limit) a Base Tier Rules + Base Extra Allowances + SUM(Add-on Tier Rules) forrásokból.
|
||||
|
||||
### 🔧 Módosított fájlok
|
||||
|
||||
**1. `backend/app/models/core_logic.py`** — `SubscriptionTier.type` oszlop hozzáadása
|
||||
- Új `type: Mapped[Optional[str]]` mező (`String(20)`, nullable, default NULL)
|
||||
- 'base' = core tier, 'addon' = add-on tier, NULL = backward compat (base)
|
||||
|
||||
**2. `backend/app/api/v1/endpoints/admin_organizations.py`** — Multi-subscription aggregation
|
||||
- `SubscriptionSummary.active_addons: List[str]` mező hozzáadása
|
||||
- `get_organization_details`: `.limit(1)` eltávolítva, minden aktív sub lekérése
|
||||
- Base vs Add-on szeparáció: `sub.tier.type` és `rules.get("type")` alapján
|
||||
- Aggregált limit számítás: `asset_limit = base_vehicles + extra_vehicles + addon_vehicles`
|
||||
- `max(asset_limit, 1)` fallback csak a végösszegre alkalmazva
|
||||
- `list_organizations`: bulk query-ben is base/addon szeparáció
|
||||
|
||||
### ✅ Verifikáció
|
||||
- Sync Engine: ✅ 1279 OK, 0 Fixed, 0 Extra
|
||||
- Python syntax: ✅ Mindkét fájl hibátlanul compile-ol
|
||||
- Subscription feature flags test: ✅ ALL TESTS COMPLETED
|
||||
|
||||
## 2026-06-27 - P0 HOTFIX: Subscription PATCH 422 Error & Add-on Only Update
|
||||
|
||||
### 🎯 Cél
|
||||
Két bug javítása a Subscription PATCH flow-ban:
|
||||
1. **Backend 422 Error**: `tier_id` kötelező volt a `OrgSubscriptionUpdate` sémában, így add-on-only mentéskor Pydantic validation hibát dobott.
|
||||
2. **Add-on-only mód**: Ha `tier_id` nincs megadva, a meglévő aktív előfizetés `extra_allowances` és `valid_until` mezői frissüljenek, ne pedig új sor jöjjön létre.
|
||||
|
||||
### 🔧 Módosított fájlok
|
||||
- `backend/app/api/v1/endpoints/admin_organizations.py`
|
||||
- **Schema** (47-68. sor): `tier_id: int = Field(...)` → `tier_id: Optional[int] = Field(default=None, ...)`
|
||||
- **Endpoint** (1067-1240. sor): Két üzemmódra bontás:
|
||||
- **A) tier_id megadva**: Teljes csomagváltás (meglévő logika)
|
||||
- **B) tier_id = None**: Aktív előfizetés `extra_allowances` és `valid_until` frissítése `sa_update`-dal
|
||||
- `frontend_admin/pages/garages/[id]/index.vue` — Nem volt szükség módosításra, a frontend payload konstrukciója már helyes volt.
|
||||
|
||||
### ✅ Verifikáció
|
||||
- Python syntax: ✅ Sikeres compile
|
||||
- Sync Engine: ✅ 1279 OK, 0 Fixed, 0 Extra — teljes szinkronban
|
||||
|
||||
## 2026-06-27 13:16 UTC — P0 HOTFIX: 422 Tier ID & Missing Expiration Payload
|
||||
|
||||
**Gitea Card:** #302
|
||||
|
||||
**Problem:** PATCH /admin/organizations/{org_id}/subscription returned 422 when tier_id was omitted (add-on-only update). Frontend omitted expires_at when no base tier selected.
|
||||
|
||||
**Root Cause (BACKEND):** `OrgSubscriptionUpdate.tier_id` was `int = Field(...)` (required). Changed to `Optional[int] = Field(default=None)`. Endpoint split into Mode A (tier_id present → full switch) and Mode B (tier_id=None → add-on/expiry update).
|
||||
|
||||
**Root Cause (FRONTEND):** Button disabled on `!selectedTierId` only. Payload construction didn't handle tierless updates. Fixed: linear payload, `addonsChanged` computed, always send extra_allowances.
|
||||
|
||||
**CRITICAL Deployment Fix:** Code changes were on disk but containers were NOT restarted:
|
||||
- sf_api (23h uptime, uvicorn without --reload)
|
||||
- sf_admin_frontend (24h uptime)
|
||||
Both restarted at 13:13 UTC to apply fixes.
|
||||
|
||||
**Verification:** All 4 Pydantic schema validation tests PASS.
|
||||
|
||||
## 2026-06-27 - P0 Hotfix: 500 Internal Server Error on GET /admin/organizations/{id}/details
|
||||
|
||||
### 🎯 Cél
|
||||
A garázs részletek betöltésekor (GET /api/v1/admin/organizations/43/details) 500-as hiba jelentkezett. A korábbi hotfix (422 Tier ID fix) során hozzáadott lazy importok hibás elérési útvonalakat tartalmaztak.
|
||||
|
||||
### 🔧 Javítások (admin_organizations.py)
|
||||
1. **Line 490:** `from app.models.fleet.vehicle import Vehicle` → `from app.models import Vehicle` (Asset class a `app.models.vehicle.asset`-ben, `Vehicle` néven exportálva a `app.models`-ből)
|
||||
2. **Line 492:** `Vehicle.organization_id` → `Vehicle.current_organization_id` (Asset modellben nincs `organization_id`, helyette `current_organization_id`)
|
||||
3. **Line 493:** `Vehicle.is_deleted == False` törölve (Asset modellben nincs `is_deleted` mező)
|
||||
4. **Line 499:** `from app.models.fleet.organization import Branch` → `from app.models.marketplace.organization import Branch` (Branch a marketplace sémában van)
|
||||
5. **Line 155 (BranchBrief.id):** `int` → `str` (Branch elsődleges kulcsa UUID, nem integer)
|
||||
6. **Line 507:** `b.id` → `str(b.id)` (UUID konvertálása stringgé)
|
||||
7. **Line 510:** `b.is_active` → `not b.is_deleted` (Branch modellben nincs `is_active`, helyette `is_deleted` boolean)
|
||||
|
||||
### ✅ Verifikáció
|
||||
- `GET /api/v1/admin/organizations/43/details` → **200 OK** (34 mezővel, köztük branches, members, subscription)
|
||||
- Minden import sikeresen betöltődik
|
||||
|
||||
## 2026-06-27 - P0 JSONB NESTING HOTFIX: Allowances Object & i18n Keys
|
||||
|
||||
### 🎯 Cél
|
||||
Fix the `_extract_limit` helper to also search inside a nested `"allowances"` dict within the tier rules JSONB, and add missing i18n keys for the add-on input sub-labels.
|
||||
|
||||
### 🔧 Változtatások
|
||||
|
||||
**1. Backend — Nested JSONB lookup**
|
||||
- Fájl: `backend/app/api/v1/endpoints/admin_organizations.py` (line 601)
|
||||
- Módosítás: `_extract_limit()` helper now checks `rules.get("allowances", {})` after checking root-level keys.
|
||||
- Hatás: Tier limits stored as `{"allowances": {"max_vehicles": 20}}` are now correctly extracted.
|
||||
|
||||
**2. Frontend — Missing i18n keys**
|
||||
- Fájlok: `frontend_admin/i18n/locales/en.json`, `frontend_admin/i18n/locales/hu.json`
|
||||
- Hozzáadva: `garages.details.branches` ("branches"/"telephely") és `garages.details.employees` ("employees"/"dolgozó")
|
||||
- Hatás: A `+db {{ $t('garages.details.branches') }}` és `+db {{ $t('garages.details.employees') }}` feliratok már nem jelennek meg nyers kulcsként.
|
||||
|
||||
### ✅ Verifikáció
|
||||
- Python syntax check: ✅ OK
|
||||
- Sync engine: 1279 OK, 0 Fixed, 0 Shadow — teljes szinkronban
|
||||
|
||||
## 2026-06-27 - P0 ARCHITECTURE UNIFICATION: Eradicate Path A & Fix Limit(1)
|
||||
|
||||
### 🎯 Cél
|
||||
A backend `admin_organizations.py` fájlban két külön kódút (Path A és Path B) számolta a subscription limiteket. Path A (a `get_organization_details` végpontban) még a régi `rules.get("max_vehicles")` logikát használta ahelyett, hogy az `_extract_limit()` helpert hívta volna, ami a JSONB `allowances` nested objektumba is belenéz. Emellett a subscription query `.limit(1)`-et használt, ami megtörte a multi-subscription (Base + Add-on) aggregációt.
|
||||
|
||||
### 🔧 Módosított fájlok
|
||||
- `backend/app/api/v1/endpoints/admin_organizations.py` — Három kritikus változtatás:
|
||||
|
||||
**1. `_extract_limit()` helper létrehozása** (lines 397-447)
|
||||
- Új, robusztus függvény a limit értékek kinyerésére a `SubscriptionTier.rules` JSONB-ból.
|
||||
- Három szinten keres: (1) közvetlenül a `rules` objektumban, (2) `rules["allowances"]` nested objektumban, (3) `rules["limits"]` nested objektumban.
|
||||
- Több kulcsot is támogat (pl. `["max_vehicles", "max_assets"]`), az első találatot adja vissza.
|
||||
|
||||
**2. `.limit(1)` eltávolítása a subscription query-ből** (lines 499-514)
|
||||
- A subscription lekérdezés most `.scalars().all()`-t használ `.limit(1)` helyett.
|
||||
- Minden aktív subscription sort visszaad (1 Base + N Add-on).
|
||||
|
||||
**3. Path A és Path B egyesítése** (lines 574-691)
|
||||
- A régi Path A (`if active_sub and active_sub.tier:`) és Path B (`elif org.subscription_tier:`) logika helyett egységes, `_extract_limit()`-t használó kód.
|
||||
- Base subscription-ok és Add-on subscription-ok szétválasztása a `tier.type` mező alapján.
|
||||
- Add-on tier-ek base limitjeinek aggregálása a primary subscription limitjeihez.
|
||||
- `extra_allowances` aggregálása az összes aktív subscription-ből.
|
||||
|
||||
### ✅ Verifikáció
|
||||
- Python syntax check: ✅ OK (`py_compile` sikeres)
|
||||
- Sync engine: 1279 OK, 0 Fixed, 0 Shadow — teljes szinkronban
|
||||
- A `SubscriptionSummary` most helyesen kapja meg a Base limit értékeket az `_extract_limit()`-en keresztül, mielőtt az Extra allowances hozzáadódnak.
|
||||
|
||||
## 2026-06-29 - Gitea #319: Felhasználói trend diagram komponens (EPIC-5, task 5.2)
|
||||
|
||||
### 🎯 Cél
|
||||
Felhasználói regisztrációs trend diagram komponens elkészítése az admin felület users list oldalára.
|
||||
|
||||
### 🔧 Változtatások
|
||||
|
||||
**1. UserTrendChart.vue komponens létrehozása:** `frontend_admin/components/charts/UserTrendChart.vue`
|
||||
- Pure SVG bar chart (nincs külső chart library függőség)
|
||||
- Period selector: 7/14/30/90 nap
|
||||
- Grid lines, Y-axis labels, hover tooltips
|
||||
- Trend direction indicator (up/down/stable)
|
||||
- Summary stats: daily avg, total, trend %
|
||||
- Loading és empty state kezelés
|
||||
|
||||
**2. i18n kulcsok hozzáadása:** `frontend_admin/locales/hu.json`, `frontend_admin/locales/en.json`
|
||||
- trend_title, trend_subtitle, trend_7d/14d/30d/90d
|
||||
- trend_no_data, trend_daily, trend_avg, trend_total, trend_vs_prev
|
||||
|
||||
**3. Integrálás a users oldalba:** `frontend_admin/pages/users/index.vue`
|
||||
- Import: `import UserTrendChart from '~/components/charts/UserTrendChart.vue'`
|
||||
- Template: `<UserTrendChart :data="stats.registration_trend" :loading="loading" />`
|
||||
- A stats kártyák és a filter bar közé helyezve
|
||||
|
||||
### ✅ Eredmény
|
||||
- A backend `GET /admin/users/stats` endpoint már létezik és visszaadja a `registration_trend` adatokat
|
||||
- A frontend build hibátlanul lefutott a `sf_admin_frontend` konténerben
|
||||
- A komponens automatikusan használja a meglévő `stats.registration_trend` adatokat
|
||||
|
||||
## 2026-06-29 - EPIC-6 Task 6.1: i18n Fordítások Felvétele (hu/en) - User & Person Oldalak
|
||||
|
||||
### 🎯 Cél
|
||||
i18n fordítások (hu/en) hozzáadása a frontend_admin user és person kezelő oldalakhoz a [`plans/logic_spec_admin_user_person_management.md`](plans/logic_spec_admin_user_person_management.md) specifikáció alapján (EPIC-6, task 6.1).
|
||||
|
||||
### 🔧 Módosított fájlok
|
||||
- [`frontend_admin/locales/en.json`](frontend_admin/locales/en.json) - Teljes `users` szekció hozzáadva (users.* és users.details.* kulcsok); JSON szintaxis hiba javítva (hiányzó vessző a persons.details után)
|
||||
- [`frontend_admin/locales/hu.json`](frontend_admin/locales/hu.json) - Teljes `users` szekció hozzáadva magyar fordításokkal; JSON szintaxis hiba javítva
|
||||
|
||||
### ✅ Eredmények
|
||||
- **JSON validáció:** ✅ Sikeres (python3 json.load)
|
||||
- **Nuxt build:** ✅ Sikeres (6.78s, AdminUsersView chunk: 19.93 kB)
|
||||
- **Hiányzó kulcsok pótolva:** `users.title`, `users.subtitle`, `users.search_placeholder`, `users.*` (status, role, plan filterek), `users.details.*` (profil, subscription, személyes adatok, tagságok)
|
||||
- **JSON hiba javítva:** Hiányzó vessző a `persons.details` objektum záró kapcsos zárójele után (224. sor) mindkét locale fájlban
|
||||
|
||||
## 2026-06-29 — RBAC Permission Check Implementation (Gitea #321)
|
||||
|
||||
### 🎯 Cél
|
||||
Card #321: "6.2: RBAC permission check implementáció" — Minden admin user/person oldalnak ellenőriznie kell a DB-driven RBAC permissionöket a spec szerint.
|
||||
|
||||
### 🔧 Módosított fájlok
|
||||
- [`frontend_admin/pages/users/index.vue`](frontend_admin/pages/users/index.vue) — `useAuthStore`/`useRouter` importok, `canViewUsers`/`canEditUsers`/`canBanUsers`/`canDeleteUsers` computed property-k, redirect guard `users:view` hiányában, `v-if="canEditUsers"` a bulk actions bar-on és toggle gombokon
|
||||
- [`frontend_admin/pages/users/[id]/index.vue`](frontend_admin/pages/users/%5Bid%5D/index.vue) — `hasEditPermission` átírva legacy `'user:edit'`-ről (singular) `'users:edit'`-re (plural, spec szerint)
|
||||
- [`frontend_admin/pages/persons/index.vue`](frontend_admin/pages/persons/index.vue) — `useAuthStore`/`useRouter` importok, `canViewPersons`/`canEditPersons`/`canMergePersons` computed property-k, redirect guard `persons:view` hiányában
|
||||
- [`frontend_admin/pages/persons/[id]/index.vue`](frontend_admin/pages/persons/%5Bid%5D/index.vue) — `hasEditPermission` egyszerűsítve `!!` koercícióra
|
||||
|
||||
### ✅ Verifikáció
|
||||
- **Nuxt build:** ✅ Sikeres (`docker compose exec sf_admin_frontend npm run build`)
|
||||
- **Permission kódok:** `users:view`, `users:edit`, `users:ban`, `users:delete`, `persons:view`, `persons:edit`, `persons:merge` — mind implementálva a spec (5.8) szerint
|
||||
- **Fallback:** Minden computed property `auth.isAdmin`-re esik vissza, ha nincs `system_capabilities`
|
||||
|
||||
## 2026-06-29 - Admin Users Filter Buttons Fix (#323)
|
||||
|
||||
### 🎯 Cél
|
||||
Az admin felületen a felhasználók listájában nem működő szűrő gombok (státusz, szerepkör, csomag) javítása.
|
||||
|
||||
### 🔧 Módosított fájlok
|
||||
- **`backend/app/api/v1/endpoints/admin.py`**:
|
||||
- `plan_filter` query paraméter hozzáadása a `list_users()` végponthoz
|
||||
- `plan_filter` szűrési logika: `User.subscription_plan == plan_filter.upper()`
|
||||
- `person_name` mező hozzáadása a válaszhoz (last_name + first_name összefűzés)
|
||||
- Case-insensitive role matching: `role.upper()` a `UserRole` enumhoz
|
||||
|
||||
- **`frontend_admin/pages/users/index.vue`**:
|
||||
- `status_filter` → `is_active`/`is_deleted` boolean mapping (switch/case)
|
||||
- `role_filter` → `role` paraméter átnevezés
|
||||
- `plan_filter` paraméter megtartva (backend most már támogatja)
|
||||
|
||||
### ✅ Eredmények
|
||||
- API szintaxis ellenőrzés: ✅ PASS
|
||||
- API végpont tesztek (7 teszt): ✅ ALL PASS
|
||||
- Basic list: 34 user, person_name megjelenik
|
||||
- role=admin (lowercase): 2 admin (case-insensitive)
|
||||
- role=user (lowercase): 30 user
|
||||
- is_active=true: 16 aktív
|
||||
- plan_filter=FREE: 28 FREE csomagos
|
||||
- Combined (role=user, is_active=true, plan_filter=PREMIUM): 2 találat
|
||||
- is_deleted=true: 1 törölt
|
||||
|
||||
## 2026-06-29 - Architect: Gamification Admin Integration Audit & Gitea Kártyák
|
||||
|
||||
### 🎯 Cél
|
||||
A meglévő Gamification rendszer (backend modellek, service-ek, API végpontok) teljes körű auditálása, komplex specifikáció készítése az admin felületre történő bekötéshez, és 15 Gitea implementációs kártya létrehozása.
|
||||
|
||||
### 🔧 Elvégzett Munka
|
||||
|
||||
**1. GAMIFICATION RENDSZER AUDIT:** ✅
|
||||
- Feltárt 10+ adatbázis tábla a `gamification` sémában (point_rules, level_configs, points_ledger, user_stats, badges, user_badges, user_contributions, seasons, seasonal_competitions, competitions, user_competition_scores)
|
||||
- Azonosított hiányzó mezők: UserStats (services_submitted, total_points), PointsLedger (xp, source_type, source_id)
|
||||
- Feltárt 14+ meglévő API végpont a `/gamification/*` útvonalon
|
||||
- Feltárt hibás végpont: `PATCH /admin/users/{id}/penalty` direkt `level` módosítás a GamificationService helyett
|
||||
- Feltárt 3 business logic probléma (szolgáltatáskikerülés, GamificationService bypass)
|
||||
|
||||
**2. KOMPLEX SPECIFIKÁCIÓ:** ✅
|
||||
- Létrehozva: [`plans/logic_spec_gamification_admin_integration.md`](plans/logic_spec_gamification_admin_integration.md)
|
||||
- 30+ új admin API endpoint terv az `admin_gamification.py` routerben
|
||||
- 12 új frontend oldal terv a `frontend_admin/pages/gamification/` mappában
|
||||
- Új Gamification menücsoport terv 6 menüponttal
|
||||
- Adatbázis séma javítások (UserStats, PointsLedger)
|
||||
- Business logic javítások (penalty endpoint, GamificationService bypass)
|
||||
- RBAC: `gamification:manage` permission
|
||||
- Adatfolyam diagram (Mermaid)
|
||||
- 15 implementációs kártya prioritási sorrenddel és függőségekkel
|
||||
|
||||
**3. GITEA KÁRTYÁK LÉTREHOZÁSA:** ✅
|
||||
15 kártya a `Epic 8 Gamification 2.0, Verseny és Önvéde` mérföldkő alatt:
|
||||
|
||||
| # | ID | Kártya Név | Scope |
|
||||
|---|----|-----------|-------|
|
||||
| 1 | #324 | Adatbázis: Gamification modell javítások (UserStats, PointsLedger) | Database |
|
||||
| 2 | #325 | Backend: Új admin_gamification.py router (30+ admin végpont) | Backend |
|
||||
| 3 | #326 | Backend: GamificationService refaktor (point_rules integráció) | Backend |
|
||||
| 4 | #327 | Backend: Meglévő PATCH /admin/users/{id}/penalty endpoint javítása | Backend |
|
||||
| 5 | #328 | Backend: gamification.py endpointok GamificationService-re átállítása | Backend |
|
||||
| 6 | #329 | Frontend: Gamification menüpont + routing az admin Nuxt felületben | Frontend |
|
||||
| 7 | #330 | Frontend: Gamification Dashboard oldal (/gamification/index.vue) | Frontend |
|
||||
| 8 | #331 | Frontend: Pontszabályok + Szintek CRUD oldalak | Frontend |
|
||||
| 9 | #332 | Frontend: Kitüntetések (Badge) kezelő oldal | Frontend |
|
||||
| 10 | #333 | Frontend: Szezonok + Versenyek kezelő oldalak | Frontend |
|
||||
| 11 | #334 | Frontend: Felhasználói statisztikák + büntetés/jutalom oldalak | Frontend |
|
||||
| 12 | #335 | Frontend: Ranglista admin nézet (leaderboard.vue) | Frontend |
|
||||
| 13 | #336 | Frontend: Master Config szerkesztő és Rendszerparaméterek oldalak | Frontend |
|
||||
| 14 | #337 | Frontend: Pontnapló böngésző oldal (ledger.vue) | Frontend |
|
||||
| 15 | #338 | Tesztelés: Gamification admin E2E tesztek | Backend+Frontend |
|
||||
|
||||
### 📂 Létrehozott fájlok
|
||||
- [`plans/logic_spec_gamification_admin_integration.md`](plans/logic_spec_gamification_admin_integration.md) (373 sor)
|
||||
|
||||
### ⚠️ Feltárt kockázatok
|
||||
1. UserStats modellből hiányzó mezők runtime hibát okozhatnak a meglévő gamification.py endpointokban
|
||||
2. A PATCH /admin/users/{id}/penalty végpont a `gamification.level` nem létező mezőt módosítja
|
||||
3. Több gamification.py végpont direktül módosít UserStats mezőket a GamificationService megkerülésével
|
||||
|
||||
258
.roo/scripts/create_gitea_cards.py
Normal file
258
.roo/scripts/create_gitea_cards.py
Normal file
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create all 18 Gitea cards for Admin User & Person Management."""
|
||||
import subprocess, sys
|
||||
|
||||
def run(cmd):
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
print(f"ERROR: {r.stderr}")
|
||||
return None
|
||||
return r.stdout.strip()
|
||||
|
||||
# Base command
|
||||
BASE = ["docker", "exec", "roo-helper", "python3", "/scripts/gitea_manager.py"]
|
||||
|
||||
MILESTONE = "Epic 10 (Admin UI) Jegyek Létrehozása"
|
||||
|
||||
cards = [
|
||||
# ===== EPIC-1: Backend User Management =====
|
||||
{
|
||||
"title": "1.1: GET /admin/users/{user_id} — Felhasználó részletek endpoint",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** REST végpont létrehozása egy felhasználó összes adatának lekérésére, beleértve Person, Address, Wallet, TrustProfile, OrganizationMembership adatokat.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** Meglévő User, Person, OrganizationMember modellek; Pydantic séma (UserResponse); require_admin függőség
|
||||
- **Kimenet (Mik támaszkodnak rá):** Frontend user részlet oldal (3.2), user szerkesztés endpoint (1.2)
|
||||
|
||||
### 📝 Elemzés
|
||||
Hiányzó endpoint: GET /admin/users/{user_id}. SQL logika: LEFT JOIN identity.users, identity.persons, identity.addresses, identity.wallets, identity.user_trust_profiles, marketplace.organization_members. Válasz tartalmazza: User alapadatok, Person adatok lakcímmel, Wallet, TrustProfile, Memberships, SystemCapabilities, OrgCapabilities. Helye: backend/app/api/v1/endpoints/admin.py a meglévő list_users endpoint mellett. Authorization: RequirePermission(permissions=['users:view'])""",
|
||||
"tags": ["Scope: Backend", "Type: Feature"]
|
||||
},
|
||||
{
|
||||
"title": "1.2: PATCH /admin/users/{user_id} — Felhasználó szerkesztése admin által",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** REST végpont admin általi felhasználó módosításhoz. Korlátozott mezők: email, is_active, is_vip, preferred_language, region_code, preferred_currency, subscription_plan, subscription_expires_at, max_vehicles, max_garages, scope_level, scope_id, role_id (csak superadmin), custom_permissions. Person almzők: last_name, first_name, phone, mothers data, birth data, identity_docs.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** GET /admin/users/{user_id} (1.1), Pydantic séma (UserUpdate), require_admin függőség
|
||||
- **Kimenet (Mik támaszkodnak rá):** Frontend user szerkesztés funkció
|
||||
|
||||
### 📝 Elemzés
|
||||
PATCH végpont a meglévő admin.py-ban. Ellenőrizni kell a rangot (superadmin vs admin). Audit log bejegyzés minden módosításról. Person adatok módosítása opcionális nested JSON-ben.""",
|
||||
"tags": ["Scope: Backend", "Type: Feature"]
|
||||
},
|
||||
{
|
||||
"title": "1.3: GET /admin/users/stats — Felhasználói statisztikák endpoint",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Dashboard statisztikák a felhasználókról: total_users, active_users, deleted_users, banned_users, new_users_today/week/month, users_by_role, users_by_plan, users_by_language, users_with_person, users_without_person, registration_trend (napi bontás), active_organizations_count, total_memberships.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** User, OrganizationMember modellek; require_admin függőség
|
||||
- **Kimenet (Mik támaszkodnak rá):** Frontend dashboard (5.1), user lista oldal statisztikai kártyái (3.1)
|
||||
|
||||
### 📝 Elemzés
|
||||
Új GET /admin/users/stats endpoint. SQL aggregációk: COUNT, GROUP BY role/plan/language. Registration trend: DATE(created_at) GROUP BY. JOIN marketplace.organization_members a memberships számhoz.""",
|
||||
"tags": ["Scope: Backend", "Type: Feature"]
|
||||
},
|
||||
{
|
||||
"title": "1.4: GET /admin/users/{user_id}/memberships — Szervezeti tagságok endpoint",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Egy felhasználó szervezeti tagságainak listázása. Válasz: organization_id, organization_name, role, status, is_permanent, expires_at, invited_email, is_verified.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** GET /admin/users/{user_id} (1.1), OrganizationMember modell
|
||||
- **Kimenet (Mik támaszkodnak rá):** Frontend user részlet oldal Memberships tab (3.2)
|
||||
|
||||
### 📝 Elemzés
|
||||
Új GET /admin/users/{user_id}/memberships endpoint. JOIN marketplace.organization_members ON user_id, majd JOIN fleet.organizations ON organization_id a névhez.""",
|
||||
"tags": ["Scope: Backend", "Type: Feature"]
|
||||
},
|
||||
# ===== EPIC-2: Backend Person Management =====
|
||||
{
|
||||
"title": "2.1: GET /admin/persons — Person lista endpoint",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Person entitások listázása kereséssel, szűréssel. Paraméterek: search (name, phone, birth data), is_ghost, is_active, has_user, is_merged, skip, limit. Válasz: total, items (id, last_name, first_name, phone, birth_date, is_ghost, is_active, merged_into_id, users_count, active_user, address).
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** Person modell; require_admin függőség
|
||||
- **Kimenet (Mik támaszkodnak rá):** Frontend Person lista oldal (4.1)
|
||||
|
||||
### 📝 Elemzés
|
||||
Új GET /admin/persons endpoint. SQL: LEFT JOIN identity.users ON person_id (users_count, active_user), LEFT JOIN identity.addresses ON address_id. Szűrés: WHERE is_ghost, is_active, merged_into_id IS NULL/IS NOT NULL.""",
|
||||
"tags": ["Scope: Backend", "Type: Feature"]
|
||||
},
|
||||
{
|
||||
"title": "2.2: GET /admin/persons/{person_id} — Person részletek endpoint",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Egy Person összes adata, kapcsolódó User-ekkel, szervezeti tagságokkal, tulajdonolt cégekkel, merge history-val.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** GET /admin/persons (2.1), Person modell
|
||||
- **Kimenet (Mik támaszkodnak rá):** Frontend Person részlet oldal (4.2), Person merge (2.4)
|
||||
|
||||
### 📝 Elemzés
|
||||
GET /admin/persons/{person_id}. Válasz: Person adatok + users lista + address + memberships + owned_business_entities + merged_into (ha merge-elt) + merged_sources (ha ő a cél).""",
|
||||
"tags": ["Scope: Backend", "Type: Feature"]
|
||||
},
|
||||
{
|
||||
"title": "2.3: PATCH /admin/persons/{person_id} — Person szerkesztése",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Person adatok admin általi módosítása. Szerkeszthető mezők: last_name, first_name, phone, mothers_last_name, mothers_first_name, birth_place, birth_date, identity_docs, ice_contact, is_active, is_ghost. Address almzők is szerkeszthetők.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** GET /admin/persons/{person_id} (2.2), PersonUpdate séma
|
||||
- **Kimenet (Mik támaszkodnak rá):** Frontend Person szerkesztés funkció
|
||||
|
||||
### 📝 Elemzés
|
||||
PATCH /admin/persons/{person_id}. Audit log bejegyzés. Address rekord frissítése ha változott.""",
|
||||
"tags": ["Scope: Backend", "Type: Feature"]
|
||||
},
|
||||
{
|
||||
"title": "2.4: POST /admin/persons/{person_id}/merge — Person merge (duplum kezelés)",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Duplum Person-ok összeolvasztása. Body: {source_person_id, keep_source_user}. Logika: (1) Ellenőrizze, hogy mindkét Person létezik és nincs merge-elve. (2) Forrás Person merged_into_id = cél Person ID. (3) Forrás User-ek person_id = cél Person ID. (4) Forrás OrganizationMember person_id = cél Person ID. (5) Audit log.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** GET /admin/persons/{person_id} (2.2), Person modell
|
||||
- **Kimenet (Mik támaszkodnak rá):** Frontend Person merge UI
|
||||
|
||||
### 📝 Elemzés
|
||||
POST /admin/persons/{target_id}/merge. Tranzakcióban: UPDATE identity.users SET person_id = target WHERE person_id = source. UPDATE marketplace.organization_members SET person_id = target WHERE person_id = source. UPDATE identity.persons SET merged_into_id = target WHERE id = source.""",
|
||||
"tags": ["Scope: Backend", "Type: Feature"]
|
||||
},
|
||||
# ===== EPIC-3: Frontend User List & Detail =====
|
||||
{
|
||||
"title": "3.1: users/index.vue — Felhasználói lista oldal (Frontend)",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Felhasználói lista oldal létrehozása a frontend admin felületen. Tartalmaz: statisztikai kártyák sor (összes, aktív, törölt, kitiltott, mai regisztrációk), keresőmező (email, név, telefon), szűrők (státusz, szerepkör, előfizetés), táblázat (ID, Email, Név, Szerepkör, Státusz, Csomag, Regisztráció, Nyelv, Műveletek), tömeges műveletek checkbox-szal.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** GET /admin/users (meglévő), GET /admin/users/stats (1.3)
|
||||
- **Kimenet (Mik támaszkodnak rá):** Navigációs menü frissítés (3.3)
|
||||
|
||||
### 📝 Elemzés
|
||||
Új fájl: frontend_admin/pages/users/index.vue. API hívás: useFetch('/api/v1/admin/users'). Tömeges művelet: POST /api/v1/admin/users/bulk-action. Statisztikák: GET /api/v1/admin/users/stats. Használja a meglévő api.ts plugint.""",
|
||||
"tags": ["Scope: Frontend", "Type: Feature"]
|
||||
},
|
||||
{
|
||||
"title": "3.2: users/[id]/index.vue — Felhasználó részlet oldal (Frontend)",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Felhasználó részlet oldal tab-okkal: (1) Áttekintés — alapadatok, státusz, szerepkör. (2) Személyes adatok — Person adatok, lakcím. (3) Pénzügyek — Wallet, előfizetés. (4) Tagságok — szervezeti tagságok. (5) Bizalom — Trust score. (6) Tevékenység — naplózott tevékenységek.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** GET /admin/users/{user_id} (1.1), GET /admin/users/{user_id}/memberships (1.4)
|
||||
- **Kimenet (Mik támaszkodnak rá):** Szerkesztés funkció
|
||||
|
||||
### 📝 Elemzés
|
||||
Új fájl: frontend_admin/pages/users/[id]/index.vue. API hívás: useFetch('/api/v1/admin/users/{id}'). Tab komponensek: OverviewTab, PersonTab, FinanceTab, MembershipsTab, TrustTab, ActivityTab.""",
|
||||
"tags": ["Scope: Frontend", "Type: Feature"]
|
||||
},
|
||||
{
|
||||
"title": "3.3: Navigációs menü frissítése — Users & Persons linkek",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Az admin oldalsáv navigációs menüjének frissítése új bejegyzésekkel: 👥 Felhasználók -> /users, ezen belül Felhasználók -> /users/ és Személyek -> /persons/.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** users/index.vue (3.1), persons/index.vue (4.1)
|
||||
- **Kimenet (Mik támaszkodnak rá):** - (nincs)
|
||||
|
||||
### 📝 Elemzés
|
||||
A meglévő sidebar/layout komponens módosítása. Új nav item-ek hozzáadása a megfelelő ikonokkal és linkekkel.""",
|
||||
"tags": ["Scope: Frontend", "Type: Feature"]
|
||||
},
|
||||
# ===== EPIC-4: Frontend Person List & Detail =====
|
||||
{
|
||||
"title": "4.1: persons/index.vue — Person lista oldal (Frontend)",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Person entitások listázása. Kereső: név, telefonszám, születési adatok. Szűrők: ghost státusz, aktív/inaktív, van-e User kapcsolat, merge státusz. Táblázat: ID, Név, Telefon, Születési dátum, User-ek száma, Aktív email, Státusz, Merge státusz, Műveletek.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** GET /admin/persons (2.1)
|
||||
- **Kimenet (Mik támaszkodnak rá):** Navigációs menü frissítés (3.3)
|
||||
|
||||
### 📝 Elemzés
|
||||
Új fájl: frontend_admin/pages/persons/index.vue. API hívás: useFetch('/api/v1/admin/persons'). Szűrő paraméterek: search, is_ghost, is_active, has_user, is_merged.""",
|
||||
"tags": ["Scope: Frontend", "Type: Feature"]
|
||||
},
|
||||
{
|
||||
"title": "4.2: persons/[id]/index.vue — Person részlet oldal (Frontend)",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Person részlet oldal: személy adatok, kapcsolódó User-ek listája, cím adatok, szervezeti tagságok, merge információ (forrás/cél), tulajdonolt cégek.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** GET /admin/persons/{person_id} (2.2)
|
||||
- **Kimenet (Mik támaszkodnak rá):** Person merge UI
|
||||
|
||||
### 📝 Elemzés
|
||||
Új fájl: frontend_admin/pages/persons/[id]/index.vue. API hívás: useFetch('/api/v1/admin/persons/{id}'). Merge gomb ha a Person nincs merge-elve.""",
|
||||
"tags": ["Scope: Frontend", "Type: Feature"]
|
||||
},
|
||||
# ===== EPIC-5: Frontend Statistics & Dashboard =====
|
||||
{
|
||||
"title": "5.1: Dashboard statisztika kártyák dinamikus adatokkal",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** A dashboard (index.vue) jelenleg hardcoded statisztikáinak (2,847 Active Users, 12,493 Total Vehicles) lecserélése dinamikus API hívásokra. Felhasználói statisztikák: GET /admin/users/stats. Jármű statisztikák: meglévő asset végpont.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** GET /admin/users/stats (1.3)
|
||||
- **Kimenet (Mik támaszkodnak rá):** - (nincs)
|
||||
|
||||
### 📝 Elemzés
|
||||
frontend_admin/pages/index.vue módosítása. useFetch('/api/v1/admin/users/stats') a user statisztikákhoz. useFetch('/api/v1/admin/vehicles/stats') a jármű statisztikákhoz.""",
|
||||
"tags": ["Scope: Frontend", "Type: Feature"]
|
||||
},
|
||||
{
|
||||
"title": "5.2: Felhasználói trend diagram komponens",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Regisztrációs trend diagram a dashboardon. A GET /admin/users/stats registration_trend mezőjéből napi bontású oszlopdiagram megjelenítése az elmúlt 30 napra.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** GET /admin/users/stats (1.3)
|
||||
- **Kimenet (Mik támaszkodnak rá):** - (nincs)
|
||||
|
||||
### 📝 Elemzés
|
||||
Chart.js vagy egyszerű SVG/CSS alapú diagram komponens. Használhatja a meglévő dashboard layout-ot.""",
|
||||
"tags": ["Scope: Frontend", "Type: Feature"]
|
||||
},
|
||||
# ===== EPIC-6: i18n & Permission Integration =====
|
||||
{
|
||||
"title": "6.1: i18n fordítások felvétele (hu/en) — User & Person management",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Új i18n fordítási kulcsok felvétele a hu.json és en.json fájlokba. Kulcsok: users.title, users.search_placeholder, users.filter_role, users.status.active/deleted/banned, users.table.id/email/name/role/status, users.bulk.ban/unban/delete/restore, persons.title, persons.search_placeholder, person.merge.title/confirm/source/target, stats.total_users/active_users/new_today.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** users/index.vue (3.1), persons/index.vue (4.1)
|
||||
- **Kimenet (Mik támaszkodnak rá):** - (nincs)
|
||||
|
||||
### 📝 Elemzés
|
||||
frontend_admin/locales/hu.json és frontend_admin/locales/en.json frissítése. A Nuxt i18n modul automatikusan betölti.""",
|
||||
"tags": ["Scope: Frontend", "Type: Feature"]
|
||||
},
|
||||
{
|
||||
"title": "6.2: RBAC permission check implementáció — User & Person oldalakon",
|
||||
"body": """**Mérföldkő:** Admin User & Person Management
|
||||
**Cél:** Minden admin user/person oldalon RBAC permission check implementálása a RequirePermission() függőséggel. Permission-ök: users:view, users:edit, users:ban, users:delete, persons:view, persons:edit, persons:merge.
|
||||
|
||||
### 🔗 Függőségek (Dependencies)
|
||||
- **Bemenet (Mikre támaszkodik):** Meglévő admin_permissions.py RBAC rendszer
|
||||
- **Kimenet (Mik támaszkodnak rá):** - (nincs)
|
||||
|
||||
### 📝 Elemzés
|
||||
Backend: RequirePermission() dekorátor hozzáadása az új endpointokhoz. Frontend: hasPermission() kompozíció használata a gombok/show/hide logikához.""",
|
||||
"tags": ["Scope: Frontend", "Type: Feature"]
|
||||
},
|
||||
]
|
||||
|
||||
for card in cards:
|
||||
title = card["title"]
|
||||
body = card["body"]
|
||||
tags = card["tags"]
|
||||
cmd = BASE + ["create", title, body, MILESTONE] + tags
|
||||
print(f"Creating: {title}")
|
||||
result = run(cmd)
|
||||
if result:
|
||||
print(f" -> {result}")
|
||||
else:
|
||||
print(f" -> FAILED")
|
||||
print()
|
||||
@@ -5,7 +5,7 @@ from app.api.v1.endpoints import (
|
||||
services, admin, expenses, evidence, social, security,
|
||||
billing, finance_admin, analytics, vehicles, system_parameters,
|
||||
gamification, translations, users, reports, dictionaries,
|
||||
admin_packages, admin_services, admin_organizations, constants, providers,
|
||||
admin_packages, admin_services, admin_organizations, admin_users, admin_persons, constants, providers,
|
||||
subscriptions, marketing, admin_permissions, regions,
|
||||
)
|
||||
|
||||
@@ -36,6 +36,8 @@ api_router.include_router(dictionaries.router, prefix="/dictionaries", tags=["Di
|
||||
api_router.include_router(admin_packages.router, prefix="/admin/packages", tags=["Admin Package Management"])
|
||||
api_router.include_router(admin_services.router, prefix="/admin/services", tags=["Admin Service Catalog"])
|
||||
api_router.include_router(admin_organizations.router, prefix="/admin/organizations", tags=["Admin Garages CRM"])
|
||||
api_router.include_router(admin_users.router, prefix="/admin/users", tags=["Admin Users"])
|
||||
api_router.include_router(admin_persons.router, prefix="/admin/persons", tags=["Admin Persons"])
|
||||
api_router.include_router(billing.router, prefix="/billing", tags=["Billing & Wallet"])
|
||||
api_router.include_router(constants.router, prefix="/constants", tags=["Constants"])
|
||||
api_router.include_router(providers.router, prefix="/providers", tags=["Providers"])
|
||||
|
||||
@@ -497,6 +497,7 @@ async def list_users(
|
||||
role: Optional[str] = Query(None, description="Szerepkör szűrés (pl. admin, user)"),
|
||||
is_active: Optional[bool] = Query(None, description="Aktív státusz szűrés"),
|
||||
is_deleted: Optional[bool] = Query(None, description="Törölt státusz szűrés"),
|
||||
plan_filter: Optional[str] = Query(None, description="Csomag szűrés (pl. FREE, PREMIUM, ENTERPRISE)"),
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
current_user: User = Depends(deps.get_current_active_user),
|
||||
_ = Depends(deps.RequirePermission("user:view")),
|
||||
@@ -580,8 +581,10 @@ async def list_users(
|
||||
)
|
||||
|
||||
if role:
|
||||
# Case-insensitive role matching
|
||||
role_upper = role.upper()
|
||||
try:
|
||||
role_enum = UserRole(role)
|
||||
role_enum = UserRole(role_upper)
|
||||
query = query.where(User.role == role_enum)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
@@ -592,6 +595,8 @@ async def list_users(
|
||||
query = query.where(User.is_active == is_active)
|
||||
if is_deleted is not None:
|
||||
query = query.where(User.is_deleted == is_deleted)
|
||||
if plan_filter:
|
||||
query = query.where(User.subscription_plan == plan_filter.upper())
|
||||
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
total_result = await db.execute(count_query)
|
||||
@@ -625,6 +630,12 @@ async def list_users(
|
||||
parts.append(str(address.house_number))
|
||||
address_str = ', '.join(parts) if parts else None
|
||||
|
||||
# Személy nevének összefűzése
|
||||
person_name = None
|
||||
if person:
|
||||
parts = [p for p in [person.last_name, person.first_name] if p]
|
||||
person_name = ' '.join(parts) if parts else None
|
||||
|
||||
user_list.append({
|
||||
"id": u.id,
|
||||
"email": u.email,
|
||||
@@ -637,6 +648,7 @@ async def list_users(
|
||||
"created_at": u.created_at.isoformat() if u.created_at else None,
|
||||
"deleted_at": u.deleted_at.isoformat() if u.deleted_at else None,
|
||||
# Person adatok
|
||||
"person_name": person_name,
|
||||
"first_name": person.first_name if person else None,
|
||||
"last_name": person.last_name if person else None,
|
||||
"phone": person.phone if person else None,
|
||||
@@ -656,6 +668,148 @@ async def list_users(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/users/stats", tags=["User Management"])
|
||||
async def get_user_stats(
|
||||
db: AsyncSession = Depends(deps.get_db),
|
||||
current_user: User = Depends(deps.get_current_active_user),
|
||||
_ = Depends(deps.RequirePermission("user:view")),
|
||||
):
|
||||
"""
|
||||
Felhasználói statisztikák a Dashboard számára.
|
||||
|
||||
Visszaadja az összesítő adatokat: teljes, aktív, törölt, kitiltott felhasználók,
|
||||
mai/héti/havi új regisztrációk, szerepkör és előfizetés szerinti megoszlás,
|
||||
nyelvi statisztikák, Person kapcsolatok, szervezeti adatok.
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
|
||||
today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
week_start = today_start - timedelta(days=today_start.weekday())
|
||||
month_start = today_start.replace(day=1)
|
||||
|
||||
# === ALAP STATISZTIKÁK ===
|
||||
total_result = await db.execute(select(func.count(User.id)))
|
||||
total_users = total_result.scalar() or 0
|
||||
|
||||
active_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.is_active == True, User.is_deleted == False)
|
||||
)
|
||||
active_users = active_result.scalar() or 0
|
||||
|
||||
deleted_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.is_deleted == True)
|
||||
)
|
||||
deleted_users = deleted_result.scalar() or 0
|
||||
|
||||
# Banned = is_active=False AND is_deleted=False (deactivated but not deleted)
|
||||
banned_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.is_active == False, User.is_deleted == False)
|
||||
)
|
||||
banned_users = banned_result.scalar() or 0
|
||||
|
||||
# === ÚJ REGISZTRÁCIÓK ===
|
||||
new_today_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.created_at >= today_start)
|
||||
)
|
||||
new_users_today = new_today_result.scalar() or 0
|
||||
|
||||
new_week_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.created_at >= week_start)
|
||||
)
|
||||
new_users_this_week = new_week_result.scalar() or 0
|
||||
|
||||
new_month_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.created_at >= month_start)
|
||||
)
|
||||
new_users_this_month = new_month_result.scalar() or 0
|
||||
|
||||
# === SZEREPKÖR SZERINTI MEGOSZLÁS ===
|
||||
role_result = await db.execute(
|
||||
select(User.role, func.count(User.id)).group_by(User.role)
|
||||
)
|
||||
users_by_role = {}
|
||||
for row in role_result:
|
||||
role_key = row[0].value if hasattr(row[0], "value") else str(row[0])
|
||||
users_by_role[role_key] = row[1]
|
||||
|
||||
# === ELŐFIZETÉS SZERINTI MEGOSZLÁS ===
|
||||
plan_result = await db.execute(
|
||||
select(User.subscription_plan, func.count(User.id)).group_by(User.subscription_plan)
|
||||
)
|
||||
users_by_plan = {row[0]: row[1] for row in plan_result}
|
||||
|
||||
# === NYELV SZERINTI MEGOSZLÁS ===
|
||||
lang_result = await db.execute(
|
||||
select(User.preferred_language, func.count(User.id)).group_by(User.preferred_language)
|
||||
)
|
||||
users_by_language = {row[0]: row[1] for row in lang_result}
|
||||
|
||||
# === PERSON KAPCSOLATOK ===
|
||||
with_person_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.person_id.isnot(None))
|
||||
)
|
||||
users_with_person = with_person_result.scalar() or 0
|
||||
|
||||
users_without_person = total_users - users_with_person
|
||||
|
||||
# === SZERVEZETI STATISZTIKÁK ===
|
||||
from app.models.marketplace.organization import Organization, OrganizationMember
|
||||
|
||||
org_count_result = await db.execute(
|
||||
select(func.count(Organization.id)).where(Organization.is_deleted == False)
|
||||
)
|
||||
active_organizations_count = org_count_result.scalar() or 0
|
||||
|
||||
memberships_result = await db.execute(
|
||||
select(func.count(OrganizationMember.id))
|
||||
)
|
||||
total_memberships = memberships_result.scalar() or 0
|
||||
|
||||
# === JÁRMŰ STATISZTIKÁK ===
|
||||
from app.models.vehicle.asset import Asset
|
||||
|
||||
vehicle_count_result = await db.execute(
|
||||
select(func.count(Asset.id))
|
||||
)
|
||||
total_vehicles = vehicle_count_result.scalar() or 0
|
||||
|
||||
# === REGISZTRÁCIÓS TREND (utolsó 30 nap) ===
|
||||
thirty_days_ago = today_start - timedelta(days=30)
|
||||
trend_result = await db.execute(
|
||||
text("""
|
||||
SELECT DATE(created_at) as reg_date, COUNT(*) as cnt
|
||||
FROM identity.users
|
||||
WHERE created_at >= :start_date
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY reg_date
|
||||
"""),
|
||||
{"start_date": thirty_days_ago}
|
||||
)
|
||||
registration_trend = [
|
||||
{"date": row[0].isoformat() if hasattr(row[0], 'isoformat') else str(row[0]), "count": row[1]}
|
||||
for row in trend_result
|
||||
]
|
||||
|
||||
return {
|
||||
"total_users": total_users,
|
||||
"active_users": active_users,
|
||||
"deleted_users": deleted_users,
|
||||
"banned_users": banned_users,
|
||||
"new_users_today": new_users_today,
|
||||
"new_users_this_week": new_users_this_week,
|
||||
"new_users_this_month": new_users_this_month,
|
||||
"users_by_role": users_by_role,
|
||||
"users_by_plan": users_by_plan,
|
||||
"users_by_language": users_by_language,
|
||||
"users_with_person": users_with_person,
|
||||
"users_without_person": users_without_person,
|
||||
"total_vehicles": total_vehicles,
|
||||
"active_organizations_count": active_organizations_count,
|
||||
"total_memberships": total_memberships,
|
||||
"registration_trend": registration_trend,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/users/bulk-action", tags=["User Management"])
|
||||
async def bulk_user_action(
|
||||
request: BulkActionRequest,
|
||||
@@ -732,11 +886,11 @@ async def bulk_user_action(
|
||||
affected_count += 1
|
||||
|
||||
audit_log = SecurityAuditLog(
|
||||
user_id=current_user.id,
|
||||
actor_id=current_user.id,
|
||||
action=f"bulk_{request.action}",
|
||||
details=f"Bulk action '{request.action}' on {affected_count} users. IDs: {request.user_ids}",
|
||||
payload_before={},
|
||||
payload_after={"details": f"Bulk action '{request.action}' on {affected_count} users. IDs: {request.user_ids}"},
|
||||
is_critical=(request.action in ("hard_delete", "ban")),
|
||||
ip_address="admin_api"
|
||||
)
|
||||
db.add(audit_log)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -229,6 +229,7 @@ async def create_package(
|
||||
|
||||
tier = SubscriptionTier(
|
||||
name=payload.name,
|
||||
type=payload.type,
|
||||
rules=rules_dict,
|
||||
is_custom=payload.is_custom,
|
||||
tier_level=payload.tier_level,
|
||||
@@ -309,6 +310,9 @@ async def update_package(
|
||||
)
|
||||
tier.name = update_data["name"]
|
||||
|
||||
if "type" in update_data and update_data["type"] is not None:
|
||||
tier.type = update_data["type"]
|
||||
|
||||
if "rules" in update_data and update_data["rules"] is not None:
|
||||
# ── P0 CRITICAL FIX: Deep merge instead of wholesale replace ──
|
||||
# Previously: tier.rules = new_rules (wholesale replacement)
|
||||
|
||||
1084
backend/app/api/v1/endpoints/admin_persons.py
Normal file
1084
backend/app/api/v1/endpoints/admin_persons.py
Normal file
File diff suppressed because it is too large
Load Diff
646
backend/app/api/v1/endpoints/admin_users.py
Normal file
646
backend/app/api/v1/endpoints/admin_users.py
Normal file
@@ -0,0 +1,646 @@
|
||||
"""
|
||||
👤 Admin Users API
|
||||
|
||||
Végpontok:
|
||||
GET /admin/users/stats — Felhasználói statisztikák (dashboard)
|
||||
GET /admin/users/{user_id} — Felhasználó részletes adatai (User + Person + Address)
|
||||
PATCH /admin/users/{user_id} — Felhasználó szerkesztése admin által
|
||||
GET /admin/users/{user_id}/memberships — Felhasználó szervezeti tagságai
|
||||
|
||||
Megjegyzés: A GET /admin/users (lista) és POST /admin/users/bulk-action
|
||||
végpontok a admin.py modulban találhatók, mivel az a /admin prefix alá
|
||||
van regisztrálva, és a /users, /users/bulk-action útvonalakat kezeli.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from sqlalchemy import select, func, update as sa_update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from app.api import deps
|
||||
from app.db.session import get_db
|
||||
from app.models.identity import User, Person, UserRole
|
||||
from app.models.identity.address import Address
|
||||
from app.models.marketplace.organization import Organization, OrganizationMember
|
||||
from app.models.vehicle.history import AuditLog, LogSeverity
|
||||
from app.schemas.user import UserResponse
|
||||
from app.schemas.organization import OrganizationMemberResponse
|
||||
|
||||
logger = logging.getLogger("admin-users")
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pydantic Schemas
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class RegistrationTrendItem(BaseModel):
|
||||
"""Napi regisztrációs trend elem."""
|
||||
date: str = Field(..., description="Dátum (YYYY-MM-DD)")
|
||||
count: int = Field(..., description="Regisztrációk száma")
|
||||
|
||||
|
||||
class UserStatsResponse(BaseModel):
|
||||
"""Felhasználói statisztikák a dashboard számára."""
|
||||
total_users: int = Field(..., description="Összes felhasználó")
|
||||
active_users: int = Field(..., description="Aktív felhasználók")
|
||||
deleted_users: int = Field(..., description="Törölt felhasználók")
|
||||
banned_users: int = Field(..., description="Kitiltott felhasználók")
|
||||
new_users_today: int = Field(..., description="Ma regisztrált felhasználók")
|
||||
new_users_this_week: int = Field(..., description="Ezen a héten regisztrált felhasználók")
|
||||
new_users_this_month: int = Field(..., description="Ebben a hónapban regisztrált felhasználók")
|
||||
users_by_role: Dict[str, int] = Field(..., description="Felhasználók szerepkör szerint")
|
||||
users_by_plan: Dict[str, int] = Field(..., description="Felhasználók előfizetési csomag szerint")
|
||||
users_by_language: Dict[str, int] = Field(..., description="Felhasználók nyelv szerint")
|
||||
users_with_person: int = Field(..., description="Person rekorddal rendelkező felhasználók")
|
||||
users_without_person: int = Field(..., description="Person rekord nélküli felhasználók")
|
||||
registration_trend: List[RegistrationTrendItem] = Field(
|
||||
default_factory=list, description="Regisztrációs trend (utolsó 30 nap)"
|
||||
)
|
||||
active_organizations_count: int = Field(..., description="Aktív szervezetek száma")
|
||||
total_memberships: int = Field(..., description="Összes szervezeti tagság")
|
||||
|
||||
|
||||
class AdminUserUpdate(BaseModel):
|
||||
"""Admin által szerkeszthető felhasználói mezők.
|
||||
|
||||
Csak a megadott mezők módosíthatók admin felületről.
|
||||
A role_id módosítása csak SUPERADMIN számára engedélyezett.
|
||||
"""
|
||||
email: Optional[EmailStr] = Field(default=None, description="Email cím (csak admin/superadmin)")
|
||||
is_active: Optional[bool] = Field(default=None, description="Aktív státusz (letiltás/feloldás)")
|
||||
is_vip: Optional[bool] = Field(default=None, description="VIP státusz")
|
||||
preferred_language: Optional[str] = Field(default=None, max_length=5, description="Nyelv")
|
||||
region_code: Optional[str] = Field(default=None, max_length=5, description="Régió kód")
|
||||
preferred_currency: Optional[str] = Field(default=None, max_length=3, description="Pénznem")
|
||||
subscription_plan: Optional[str] = Field(default=None, max_length=30, description="Előfizetési csomag")
|
||||
subscription_expires_at: Optional[datetime] = Field(default=None, description="Előfizetés lejárata")
|
||||
scope_level: Optional[str] = Field(default=None, max_length=30, description="Scope szint")
|
||||
scope_id: Optional[str] = Field(default=None, max_length=50, description="Scope azonosító")
|
||||
role_id: Optional[int] = Field(default=None, description="RBAC szerepkör ID (csak superadmin)")
|
||||
custom_permissions: Optional[Dict[str, Any]] = Field(default=None, description="Egyedi jogosultságok")
|
||||
|
||||
# Person almzők
|
||||
person: Optional["AdminPersonUpdate"] = Field(default=None, description="Személyes adatok")
|
||||
|
||||
|
||||
class AdminPersonUpdate(BaseModel):
|
||||
"""Admin által szerkeszthető személyes adatok."""
|
||||
last_name: Optional[str] = Field(default=None, description="Vezetéknév")
|
||||
first_name: Optional[str] = Field(default=None, description="Keresztnév")
|
||||
phone: Optional[str] = Field(default=None, description="Telefonszám")
|
||||
mothers_last_name: Optional[str] = Field(default=None, description="Anyja születési vezetékneve")
|
||||
mothers_first_name: Optional[str] = Field(default=None, description="Anyja születési keresztneve")
|
||||
birth_place: Optional[str] = Field(default=None, description="Születési hely")
|
||||
birth_date: Optional[datetime] = Field(default=None, description="Születési dátum")
|
||||
identity_docs: Optional[Dict[str, Any]] = Field(default=None, description="Személyi okmány adatok")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Endpoints
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get(
|
||||
"/stats",
|
||||
response_model=UserStatsResponse,
|
||||
summary="Felhasználói statisztikák lekérése",
|
||||
description=(
|
||||
"Visszaadja a felhasználók összesített statisztikáit a dashboard számára, "
|
||||
"beleértve a szerepkör, előfizetési csomag és nyelv szerinti megoszlást, "
|
||||
"valamint a regisztrációs trendet. Csak admin/staff jogosultsággal érhető el."
|
||||
),
|
||||
)
|
||||
async def get_user_stats(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_active_user),
|
||||
_ = Depends(deps.RequirePermission("user:view")),
|
||||
) -> UserStatsResponse:
|
||||
"""
|
||||
GET /admin/users/stats
|
||||
|
||||
Összesített felhasználói statisztikák a dashboard számára.
|
||||
Az összes lekérdezés egyetlen tranzakcióban fut le.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
week_start = today_start - timedelta(days=today_start.weekday())
|
||||
month_start = today_start.replace(day=1)
|
||||
|
||||
# ── 1. Alap statisztikák ──
|
||||
total_users_result = await db.execute(
|
||||
select(func.count(User.id))
|
||||
)
|
||||
total_users = total_users_result.scalar() or 0
|
||||
|
||||
active_users_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.is_active.is_(True), User.is_deleted.is_(False))
|
||||
)
|
||||
active_users = active_users_result.scalar() or 0
|
||||
|
||||
deleted_users_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.is_deleted.is_(True))
|
||||
)
|
||||
deleted_users = deleted_users_result.scalar() or 0
|
||||
|
||||
banned_users_result = await db.execute(
|
||||
select(func.count(User.id)).where(
|
||||
User.is_active.is_(False),
|
||||
User.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
banned_users = banned_users_result.scalar() or 0
|
||||
|
||||
# ── 2. Új felhasználók ──
|
||||
new_today_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.created_at >= today_start)
|
||||
)
|
||||
new_users_today = new_today_result.scalar() or 0
|
||||
|
||||
new_week_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.created_at >= week_start)
|
||||
)
|
||||
new_users_this_week = new_week_result.scalar() or 0
|
||||
|
||||
new_month_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.created_at >= month_start)
|
||||
)
|
||||
new_users_this_month = new_month_result.scalar() or 0
|
||||
|
||||
# ── 3. Felhasználók szerepkör szerint ──
|
||||
role_rows = await db.execute(
|
||||
select(User.role, func.count(User.id).label("cnt"))
|
||||
.group_by(User.role)
|
||||
)
|
||||
users_by_role: Dict[str, int] = {}
|
||||
for row in role_rows:
|
||||
role_name = row.role.value if hasattr(row.role, 'value') else str(row.role)
|
||||
users_by_role[role_name.lower()] = row.cnt
|
||||
|
||||
# ── 4. Felhasználók előfizetési csomag szerint ──
|
||||
plan_rows = await db.execute(
|
||||
select(User.subscription_plan, func.count(User.id).label("cnt"))
|
||||
.group_by(User.subscription_plan)
|
||||
)
|
||||
users_by_plan: Dict[str, int] = {}
|
||||
for row in plan_rows:
|
||||
users_by_plan[row.subscription_plan.lower()] = row.cnt
|
||||
|
||||
# ── 5. Felhasználók nyelv szerint ──
|
||||
lang_rows = await db.execute(
|
||||
select(User.preferred_language, func.count(User.id).label("cnt"))
|
||||
.group_by(User.preferred_language)
|
||||
)
|
||||
users_by_language: Dict[str, int] = {}
|
||||
for row in lang_rows:
|
||||
users_by_language[row.preferred_language] = row.cnt
|
||||
|
||||
# ── 6. Person kapcsolat ──
|
||||
with_person_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.person_id.isnot(None))
|
||||
)
|
||||
users_with_person = with_person_result.scalar() or 0
|
||||
|
||||
without_person_result = await db.execute(
|
||||
select(func.count(User.id)).where(User.person_id.is_(None))
|
||||
)
|
||||
users_without_person = without_person_result.scalar() or 0
|
||||
|
||||
# ── 7. Regisztrációs trend (utolsó 30 nap) ──
|
||||
thirty_days_ago = today_start - timedelta(days=30)
|
||||
day_trunc = func.date_trunc("day", User.created_at)
|
||||
trend_rows = await db.execute(
|
||||
select(
|
||||
day_trunc.label("day"),
|
||||
func.count(User.id).label("cnt"),
|
||||
)
|
||||
.where(User.created_at >= thirty_days_ago)
|
||||
.group_by(day_trunc)
|
||||
.order_by(day_trunc)
|
||||
)
|
||||
registration_trend: List[RegistrationTrendItem] = []
|
||||
for row in trend_rows:
|
||||
day_str = row.day.strftime("%Y-%m-%d") if row.day else ""
|
||||
registration_trend.append(RegistrationTrendItem(date=day_str, count=row.cnt))
|
||||
|
||||
# ── 8. Szervezeti statisztikák ──
|
||||
active_orgs_result = await db.execute(
|
||||
select(func.count(Organization.id))
|
||||
.where(Organization.is_anonymized.is_(False))
|
||||
)
|
||||
active_organizations_count = active_orgs_result.scalar() or 0
|
||||
|
||||
total_memberships_result = await db.execute(
|
||||
select(func.count(OrganizationMember.id))
|
||||
)
|
||||
total_memberships = total_memberships_result.scalar() or 0
|
||||
|
||||
return UserStatsResponse(
|
||||
total_users=total_users,
|
||||
active_users=active_users,
|
||||
deleted_users=deleted_users,
|
||||
banned_users=banned_users,
|
||||
new_users_today=new_users_today,
|
||||
new_users_this_week=new_users_this_week,
|
||||
new_users_this_month=new_users_this_month,
|
||||
users_by_role=users_by_role,
|
||||
users_by_plan=users_by_plan,
|
||||
users_by_language=users_by_language,
|
||||
users_with_person=users_with_person,
|
||||
users_without_person=users_without_person,
|
||||
registration_trend=registration_trend,
|
||||
active_organizations_count=active_organizations_count,
|
||||
total_memberships=total_memberships,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{user_id}",
|
||||
response_model=UserResponse,
|
||||
summary="Felhasználó részletes adatainak lekérése",
|
||||
description=(
|
||||
"Visszaadja egy felhasználó összes adatát, beleértve a Person rekordot "
|
||||
"és a beágyazott Address adatokat. Csak admin/staff jogosultsággal érhető el."
|
||||
),
|
||||
)
|
||||
async def get_admin_user(
|
||||
user_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_active_user),
|
||||
_ = Depends(deps.RequirePermission("user:view")),
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
GET /admin/users/{user_id}
|
||||
|
||||
Lekérdezi a felhasználót a megadott ID alapján, eager loading-gal betölti
|
||||
a Person és Address kapcsolatokat, majd visszaadja a teljes UserResponse-t.
|
||||
|
||||
Args:
|
||||
user_id: A lekérdezni kívánt felhasználó ID-ja.
|
||||
|
||||
Returns:
|
||||
UserResponse séma szerinti felhasználói adatok.
|
||||
|
||||
Raises:
|
||||
HTTPException 404: Ha a felhasználó nem található vagy törölve lett.
|
||||
"""
|
||||
stmt = (
|
||||
select(User)
|
||||
.where(User.id == user_id)
|
||||
.options(
|
||||
joinedload(User.person)
|
||||
.joinedload(Person.address)
|
||||
.joinedload(Address.postal_code)
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
user = result.unique().scalar_one_or_none()
|
||||
|
||||
if not user or user.is_deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Felhasználó nem található.",
|
||||
)
|
||||
|
||||
# Reuse the existing _build_user_response helper from users.py
|
||||
from app.api.v1.endpoints.users import _build_user_response
|
||||
|
||||
response_data = await _build_user_response(user, db=db)
|
||||
return UserResponse.model_validate(response_data)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{user_id}",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Felhasználó szerkesztése",
|
||||
description=(
|
||||
"Admin általi felhasználó szerkesztés. Csak a megadott mezők módosíthatók. "
|
||||
"A role_id módosítása csak SUPERADMIN számára engedélyezett. "
|
||||
"Szuperadmin felhasználó nem módosítható."
|
||||
),
|
||||
)
|
||||
async def update_admin_user(
|
||||
user_id: int,
|
||||
update_data: AdminUserUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_active_user),
|
||||
_ = Depends(deps.RequirePermission("user:edit")),
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
PATCH /admin/users/{user_id}
|
||||
|
||||
Admin általi felhasználó szerkesztés. Csak a megadott mezők módosíthatók.
|
||||
A role_id módosítása csak SUPERADMIN számára engedélyezett.
|
||||
Szuperadmin felhasználó nem módosítható.
|
||||
|
||||
Args:
|
||||
user_id: A módosítandó felhasználó ID-ja.
|
||||
update_data: A módosítandó mezők.
|
||||
|
||||
Returns:
|
||||
UserResponse séma szerinti frissített felhasználói adatok.
|
||||
|
||||
Raises:
|
||||
HTTPException 404: Ha a felhasználó nem található vagy törölve lett.
|
||||
HTTPException 403: Ha a módosítás nem engedélyezett.
|
||||
"""
|
||||
# ── 1. Lekérdezzük a felhasználót ──
|
||||
stmt = (
|
||||
select(User)
|
||||
.where(User.id == user_id)
|
||||
.options(
|
||||
joinedload(User.person)
|
||||
.joinedload(Person.address)
|
||||
.joinedload(Address.postal_code)
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
user = result.unique().scalar_one_or_none()
|
||||
|
||||
if not user or user.is_deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Felhasználó nem található.",
|
||||
)
|
||||
|
||||
# ── 2. Biztonsági ellenőrzések ──
|
||||
# Szuperadmin felhasználó nem módosítható (még másik superadmin által sem)
|
||||
if user.role == UserRole.SUPERADMIN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Szuperadmin felhasználó nem módosítható.",
|
||||
)
|
||||
|
||||
# role_id módosítása csak SUPERADMIN számára engedélyezett
|
||||
if update_data.role_id is not None and current_user.role != UserRole.SUPERADMIN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Csak SUPERADMIN módosíthatja a szerepkört.",
|
||||
)
|
||||
|
||||
# ── 3. Összegyűjtjük a változásokat audit loghoz ──
|
||||
old_data: Dict[str, Any] = {}
|
||||
new_data: Dict[str, Any] = {}
|
||||
|
||||
# ── 4. User mezők frissítése ──
|
||||
update_fields: Dict[str, Any] = {}
|
||||
|
||||
if update_data.email is not None:
|
||||
# Email egyediség ellenőrzés
|
||||
existing = await db.execute(
|
||||
select(User).where(User.email == update_data.email, User.id != user_id)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Ez az email cím már használatban van.",
|
||||
)
|
||||
old_data["email"] = user.email
|
||||
new_data["email"] = update_data.email
|
||||
update_fields["email"] = update_data.email
|
||||
|
||||
if update_data.is_active is not None:
|
||||
old_data["is_active"] = user.is_active
|
||||
new_data["is_active"] = update_data.is_active
|
||||
update_fields["is_active"] = update_data.is_active
|
||||
|
||||
if update_data.is_vip is not None:
|
||||
old_data["is_vip"] = user.is_vip
|
||||
new_data["is_vip"] = update_data.is_vip
|
||||
update_fields["is_vip"] = update_data.is_vip
|
||||
|
||||
if update_data.preferred_language is not None:
|
||||
old_data["preferred_language"] = user.preferred_language
|
||||
new_data["preferred_language"] = update_data.preferred_language
|
||||
update_fields["preferred_language"] = update_data.preferred_language
|
||||
|
||||
if update_data.region_code is not None:
|
||||
old_data["region_code"] = user.region_code
|
||||
new_data["region_code"] = update_data.region_code
|
||||
update_fields["region_code"] = update_data.region_code
|
||||
|
||||
if update_data.preferred_currency is not None:
|
||||
old_data["preferred_currency"] = user.preferred_currency
|
||||
new_data["preferred_currency"] = update_data.preferred_currency
|
||||
update_fields["preferred_currency"] = update_data.preferred_currency
|
||||
|
||||
if update_data.subscription_plan is not None:
|
||||
old_data["subscription_plan"] = user.subscription_plan
|
||||
new_data["subscription_plan"] = update_data.subscription_plan
|
||||
update_fields["subscription_plan"] = update_data.subscription_plan
|
||||
|
||||
if update_data.subscription_expires_at is not None:
|
||||
old_data["subscription_expires_at"] = (
|
||||
user.subscription_expires_at.isoformat() if user.subscription_expires_at else None
|
||||
)
|
||||
new_data["subscription_expires_at"] = update_data.subscription_expires_at.isoformat()
|
||||
update_fields["subscription_expires_at"] = update_data.subscription_expires_at
|
||||
|
||||
if update_data.scope_level is not None:
|
||||
old_data["scope_level"] = user.scope_level
|
||||
new_data["scope_level"] = update_data.scope_level
|
||||
update_fields["scope_level"] = update_data.scope_level
|
||||
|
||||
if update_data.scope_id is not None:
|
||||
old_data["scope_id"] = user.scope_id
|
||||
new_data["scope_id"] = update_data.scope_id
|
||||
update_fields["scope_id"] = update_data.scope_id
|
||||
|
||||
if update_data.role_id is not None:
|
||||
old_data["role_id"] = user.role_id
|
||||
new_data["role_id"] = update_data.role_id
|
||||
update_fields["role_id"] = update_data.role_id
|
||||
|
||||
if update_data.custom_permissions is not None:
|
||||
old_data["custom_permissions"] = user.custom_permissions
|
||||
new_data["custom_permissions"] = update_data.custom_permissions
|
||||
update_fields["custom_permissions"] = update_data.custom_permissions
|
||||
|
||||
# ── 5. Person mezők frissítése ──
|
||||
person_updated = False
|
||||
if update_data.person is not None and user.person is not None:
|
||||
person = user.person
|
||||
person_update_fields: Dict[str, Any] = {}
|
||||
|
||||
if update_data.person.last_name is not None:
|
||||
old_data["person.last_name"] = person.last_name
|
||||
new_data["person.last_name"] = update_data.person.last_name
|
||||
person_update_fields["last_name"] = update_data.person.last_name
|
||||
|
||||
if update_data.person.first_name is not None:
|
||||
old_data["person.first_name"] = person.first_name
|
||||
new_data["person.first_name"] = update_data.person.first_name
|
||||
person_update_fields["first_name"] = update_data.person.first_name
|
||||
|
||||
if update_data.person.phone is not None:
|
||||
old_data["person.phone"] = person.phone
|
||||
new_data["person.phone"] = update_data.person.phone
|
||||
person_update_fields["phone"] = update_data.person.phone
|
||||
|
||||
if update_data.person.mothers_last_name is not None:
|
||||
old_data["person.mothers_last_name"] = person.mothers_last_name
|
||||
new_data["person.mothers_last_name"] = update_data.person.mothers_last_name
|
||||
person_update_fields["mothers_last_name"] = update_data.person.mothers_last_name
|
||||
|
||||
if update_data.person.mothers_first_name is not None:
|
||||
old_data["person.mothers_first_name"] = person.mothers_first_name
|
||||
new_data["person.mothers_first_name"] = update_data.person.mothers_first_name
|
||||
person_update_fields["mothers_first_name"] = update_data.person.mothers_first_name
|
||||
|
||||
if update_data.person.birth_place is not None:
|
||||
old_data["person.birth_place"] = person.birth_place
|
||||
new_data["person.birth_place"] = update_data.person.birth_place
|
||||
person_update_fields["birth_place"] = update_data.person.birth_place
|
||||
|
||||
if update_data.person.birth_date is not None:
|
||||
old_data["person.birth_date"] = (
|
||||
person.birth_date.isoformat() if person.birth_date else None
|
||||
)
|
||||
new_data["person.birth_date"] = update_data.person.birth_date.isoformat()
|
||||
person_update_fields["birth_date"] = update_data.person.birth_date
|
||||
|
||||
if update_data.person.identity_docs is not None:
|
||||
old_data["person.identity_docs"] = person.identity_docs
|
||||
new_data["person.identity_docs"] = update_data.person.identity_docs
|
||||
person_update_fields["identity_docs"] = update_data.person.identity_docs
|
||||
|
||||
if person_update_fields:
|
||||
for field, value in person_update_fields.items():
|
||||
setattr(person, field, value)
|
||||
person.updated_at = datetime.now(timezone.utc)
|
||||
person_updated = True
|
||||
|
||||
# ── 6. Ha nincs változás, dobjunk hibát ──
|
||||
if not update_fields and not person_updated:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Nincs módosítandó mező.",
|
||||
)
|
||||
|
||||
# ── 7. Végrehajtjuk a frissítést ──
|
||||
if update_fields:
|
||||
stmt_update = (
|
||||
sa_update(User)
|
||||
.where(User.id == user_id)
|
||||
.values(**update_fields)
|
||||
)
|
||||
await db.execute(stmt_update)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# ── 8. Audit log ──
|
||||
try:
|
||||
audit_entry = AuditLog(
|
||||
user_id=current_user.id,
|
||||
severity=LogSeverity.info,
|
||||
action="ADMIN_USER_UPDATE",
|
||||
target_type="user",
|
||||
target_id=str(user_id),
|
||||
old_data=old_data if old_data else None,
|
||||
new_data=new_data if new_data else None,
|
||||
)
|
||||
db.add(audit_entry)
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to write audit log for user update {user_id}: {e}")
|
||||
await db.rollback()
|
||||
|
||||
# ── 9. Visszaadjuk a frissített adatokat ──
|
||||
# Újratöltjük a frissített adatokat
|
||||
stmt_refresh = (
|
||||
select(User)
|
||||
.where(User.id == user_id)
|
||||
.options(
|
||||
joinedload(User.person)
|
||||
.joinedload(Person.address)
|
||||
.joinedload(Address.postal_code)
|
||||
)
|
||||
)
|
||||
result_refresh = await db.execute(stmt_refresh)
|
||||
updated_user = result_refresh.unique().scalar_one_or_none()
|
||||
|
||||
from app.api.v1.endpoints.users import _build_user_response
|
||||
|
||||
response_data = await _build_user_response(updated_user, db=db)
|
||||
# Kibővítjük a hiányzó mezőkkel, amik a User modellen léteznek,
|
||||
# de a _build_user_response dict-ből és a UserResponse sémából hiányoznak
|
||||
response_data["is_vip"] = updated_user.is_vip
|
||||
response_data["preferred_currency"] = updated_user.preferred_currency
|
||||
response_data["custom_permissions"] = updated_user.custom_permissions
|
||||
response_data["subscription_expires_at"] = (
|
||||
updated_user.subscription_expires_at.isoformat() if updated_user.subscription_expires_at else None
|
||||
)
|
||||
response_data["scope_level"] = updated_user.scope_level or "individual"
|
||||
response_data["scope_id"] = str(updated_user.scope_id) if updated_user.scope_id else None
|
||||
return response_data
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Memberships Endpoint
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class UserMembershipResponse(BaseModel):
|
||||
"""Egy szervezeti tagság adatai a felhasználó nézetében."""
|
||||
id: int
|
||||
organization_id: int
|
||||
organization_name: str = Field(..., description="Szervezet neve")
|
||||
organization_display_name: Optional[str] = Field(default=None, description="Szervezet megjelenítési neve")
|
||||
role: str = Field(..., description="Szerepkör a szervezetben")
|
||||
status: str = Field(default="active", description="Tagság státusza")
|
||||
is_verified: bool = Field(default=False, description="Ellenőrzött tagság")
|
||||
is_permanent: bool = Field(default=False, description="Állandó tagság")
|
||||
joined_at: Optional[str] = Field(default=None, description="Csatlakozás dátuma")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{user_id}/memberships",
|
||||
response_model=List[UserMembershipResponse],
|
||||
summary="Felhasználó szervezeti tagságai",
|
||||
description=(
|
||||
"Visszaadja egy felhasználó összes szervezeti tagságát. "
|
||||
"Csak admin/staff jogosultsággal érhető el."
|
||||
),
|
||||
)
|
||||
async def get_user_memberships(
|
||||
user_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_active_user),
|
||||
_ = Depends(deps.RequirePermission("user:view")),
|
||||
) -> List[UserMembershipResponse]:
|
||||
"""
|
||||
GET /admin/users/{user_id}/memberships
|
||||
|
||||
Lekérdezi a felhasználó összes szervezeti tagságát.
|
||||
"""
|
||||
stmt = (
|
||||
select(OrganizationMember)
|
||||
.where(OrganizationMember.user_id == user_id)
|
||||
.options(joinedload(OrganizationMember.organization))
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
memberships = result.unique().scalars().all()
|
||||
|
||||
membership_list = []
|
||||
for m in memberships:
|
||||
membership_list.append(UserMembershipResponse(
|
||||
id=m.id,
|
||||
organization_id=m.organization_id,
|
||||
organization_name=m.organization.name if m.organization else "Ismeretlen",
|
||||
organization_display_name=m.organization.display_name if m.organization else None,
|
||||
role=m.role,
|
||||
status=m.status,
|
||||
is_verified=m.is_verified,
|
||||
is_permanent=m.is_permanent,
|
||||
joined_at=m.joined_at.isoformat() if m.joined_at else None,
|
||||
))
|
||||
|
||||
return membership_list
|
||||
@@ -69,6 +69,10 @@ class SubscriptionTier(Base):
|
||||
"""
|
||||
Előfizetési csomagok definíciója (pl. Free, Premium, VIP).
|
||||
A csomagok határozzák meg a korlátokat (pl. max járműszám).
|
||||
|
||||
P0 MULTI-SUBSCRIPTION AGGREGATION:
|
||||
A `type` mező különbözteti meg a Base tier-eket ('private', 'corporate', 'business')
|
||||
az Add-on tier-ektől ('addon'). Ez lehetővé teszi az "1 Base + N Add-on" modellt.
|
||||
"""
|
||||
__tablename__ = "subscription_tiers"
|
||||
__table_args__ = {"schema": "system"}
|
||||
@@ -78,6 +82,18 @@ class SubscriptionTier(Base):
|
||||
rules: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) # pl. {"max_vehicles": 5}
|
||||
is_custom: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# ── P0 MULTI-SUBSCRIPTION AGGREGATION: Base vs Add-on típus ──
|
||||
# 'base' = core tier (individual, corporate, business)
|
||||
# 'addon' = add-on tier (extra vehicles, branches, users)
|
||||
# None/empty = treated as 'base' for backward compatibility
|
||||
type: Mapped[Optional[str]] = mapped_column(
|
||||
String(20),
|
||||
nullable=True,
|
||||
default=None,
|
||||
server_default=text("NULL"),
|
||||
comment="Tier type: 'base' for core subscriptions, 'addon' for add-on tiers. NULL = base (backward compat)."
|
||||
)
|
||||
|
||||
# ── P0 3D CAPABILITY MATRIX: feature_capabilities JSONB ──
|
||||
# Defines what features/capabilities this subscription tier unlocks.
|
||||
# Example: {"can_export_data": True, "can_use_analytics": True, "max_vehicles": 50}
|
||||
|
||||
@@ -46,9 +46,10 @@ class PricingModel(BaseModel):
|
||||
|
||||
class AllowancesModel(BaseModel):
|
||||
"""Korlátok és keretek a csomagban."""
|
||||
max_vehicles: int = Field(..., ge=0, description="Maximális járműszám")
|
||||
max_garages: int = Field(..., ge=0, description="Maximális garázsok száma")
|
||||
monthly_free_credits: int = Field(..., ge=0, description="Havi ingyenes kreditek száma")
|
||||
max_vehicles: int = Field(default=0, ge=0, description="Maximális járműszám")
|
||||
max_garages: int = Field(default=0, ge=0, description="Maximális garázsok száma")
|
||||
max_users: int = Field(default=0, ge=0, description="Maximális dolgozók/felhasználók száma (0 = nincs korlátozva)")
|
||||
monthly_free_credits: int = Field(default=0, ge=0, description="Havi ingyenes kreditek száma")
|
||||
|
||||
|
||||
class DurationModel(BaseModel):
|
||||
@@ -100,7 +101,7 @@ class SubscriptionRulesModel(BaseModel):
|
||||
ad_policy: Hirdetési szabályok (show_ads, ad_free_grace_days)
|
||||
marketing: Marketing adatok a kártya megjelenítéshez (subtitle, badge, highlight_color)
|
||||
"""
|
||||
type: str = Field(..., pattern=r"^(private|corporate)$", description="Csomag típusa")
|
||||
type: Optional[str] = Field(default=None, pattern=r"^(private|corporate|addon)$", description="Csomag típusa (private/corporate/addon)")
|
||||
display_name: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Emberi olvasásra szánt megjelenítési név (pl. 'Privát Ingyenes')",
|
||||
@@ -141,9 +142,11 @@ class SubscriptionRulesModel(BaseModel):
|
||||
|
||||
@field_validator("type")
|
||||
@classmethod
|
||||
def validate_type(cls, v: str) -> str:
|
||||
if v.lower() not in ("private", "corporate"):
|
||||
raise ValueError("A 'type' mező csak 'private' vagy 'corporate' lehet.")
|
||||
def validate_type(cls, v: Optional[str]) -> Optional[str]:
|
||||
if v is None:
|
||||
return v
|
||||
if v.lower() not in ("private", "corporate", "addon"):
|
||||
raise ValueError("A 'type' mező csak 'private', 'corporate' vagy 'addon' lehet.")
|
||||
return v.lower()
|
||||
|
||||
|
||||
@@ -184,6 +187,7 @@ class PublicSubscriptionTierResponse(BaseModel):
|
||||
class SubscriptionTierCreate(BaseModel):
|
||||
"""Új előfizetési csomag létrehozása (POST /)."""
|
||||
name: str = Field(..., min_length=2, max_length=100, description="Csomag neve (pl. private_pro_v2)")
|
||||
type: Optional[str] = Field(default="base", description="Csomag típusa: 'base' (alap) vagy 'addon' (kiegészítő)")
|
||||
rules: SubscriptionRulesModel
|
||||
is_custom: bool = Field(default=False, description="Egyedi (custom) csomag-e")
|
||||
tier_level: int = Field(default=0, ge=0, description="Entitlement hierarchy level: 0=free, 1=premium, 2=enterprise")
|
||||
@@ -202,6 +206,7 @@ class SubscriptionTierCreate(BaseModel):
|
||||
class SubscriptionTierUpdate(BaseModel):
|
||||
"""Meglévő előfizetési csomag részleges frissítése (PATCH /{tier_id})."""
|
||||
name: Optional[str] = Field(default=None, min_length=2, max_length=100, description="Új csomagnév")
|
||||
type: Optional[str] = Field(default=None, description="Csomag típusa: 'base' vagy 'addon'")
|
||||
rules: Optional[SubscriptionRulesModel] = Field(default=None, description="Frissített rules JSONB")
|
||||
is_custom: Optional[bool] = Field(default=None, description="Egyedi csomag jelölés")
|
||||
tier_level: Optional[int] = Field(default=None, ge=0, description="Entitlement hierarchy level")
|
||||
|
||||
@@ -670,9 +670,13 @@ class AssetService:
|
||||
A művelet:
|
||||
1. Frissíti a current_mileage értékét a megadott final_mileage-ra
|
||||
2. Állítja: status = 'archived'
|
||||
3. Nullázza az owner_person_id és owner_org_id mezőket
|
||||
3. Nullázza az owner_person_id, owner_org_id és current_organization_id mezőket
|
||||
4. Elmenti az archive_info metaadatokat az individual_equipment JSONB-be
|
||||
5. Naplózza a biztonsági auditba
|
||||
|
||||
P0 CRITICAL FIX: current_organization_id is also cleared so the vehicle
|
||||
no longer counts toward the org's active vehicle quota. The previous
|
||||
value is preserved in archive_info for audit/historical purposes.
|
||||
"""
|
||||
from app.models.identity import User
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -712,14 +716,19 @@ class AssetService:
|
||||
'archived_at': now_iso,
|
||||
'previous_owner_person_id': asset.owner_person_id,
|
||||
'previous_owner_org_id': asset.owner_org_id,
|
||||
'previous_current_organization_id': asset.current_organization_id,
|
||||
'final_mileage': final_mileage,
|
||||
}
|
||||
|
||||
# 5. Frissítjük a jármű adatait
|
||||
# P0 CRITICAL FIX: Clear current_organization_id so the vehicle no longer
|
||||
# counts toward the org's active vehicle quota. The previous value is
|
||||
# preserved in archive_info above for historical audit purposes.
|
||||
asset.current_mileage = final_mileage
|
||||
asset.status = "archived"
|
||||
asset.owner_person_id = None
|
||||
asset.owner_org_id = None
|
||||
asset.current_organization_id = None
|
||||
asset.individual_equipment = equipment
|
||||
asset.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
|
||||
197
backend/tests/test_admin_user_patch.py
Normal file
197
backend/tests/test_admin_user_patch.py
Normal file
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for PATCH /admin/users/{user_id} endpoint.
|
||||
Uses form-data for OAuth2 login, then tests the admin user update endpoint.
|
||||
"""
|
||||
import httpx
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
|
||||
BASE_URL = "http://sf_api:8000/api/v1"
|
||||
|
||||
async def test_admin_user_patch():
|
||||
async with httpx.AsyncClient(base_url=BASE_URL) as client:
|
||||
# Step 1: Login as admin using form data (OAuth2PasswordRequestForm)
|
||||
print("=" * 60)
|
||||
print("STEP 1: Login as admin")
|
||||
print("=" * 60)
|
||||
|
||||
login_data = {
|
||||
"username": "admin@profibot.hu",
|
||||
"password": "Admin123!",
|
||||
"remember_me": "true"
|
||||
}
|
||||
|
||||
r = await client.post("/auth/login", data=login_data)
|
||||
print(f"Status: {r.status_code}")
|
||||
|
||||
if r.status_code != 200:
|
||||
print(f"Login failed: {r.text}")
|
||||
return False
|
||||
|
||||
token_data = r.json()
|
||||
access_token = token_data.get("access_token")
|
||||
print(f"Token received: {access_token[:50]}...")
|
||||
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
|
||||
# Step 2: Try to find a non-superadmin user
|
||||
print("\n" + "=" * 60)
|
||||
print("STEP 2: Find a non-superadmin user")
|
||||
print("=" * 60)
|
||||
|
||||
# Try user IDs 2, 3, 4, etc. until we find a regular user
|
||||
test_user_id = None
|
||||
for uid in [2, 3, 4, 5]:
|
||||
r = await client.get(f"/admin/users/{uid}", headers=headers)
|
||||
if r.status_code == 200:
|
||||
user_data = r.json()
|
||||
role = user_data.get("role", "")
|
||||
print(f" User ID={uid}: email={user_data.get('email')}, role={role}")
|
||||
if role != "superadmin":
|
||||
test_user_id = uid
|
||||
break
|
||||
else:
|
||||
print(f" User ID={uid}: Status={r.status_code}")
|
||||
|
||||
if test_user_id is None:
|
||||
print("No non-superadmin user found. Using user ID 2 as fallback.")
|
||||
test_user_id = 2
|
||||
|
||||
# Get the user details
|
||||
r = await client.get(f"/admin/users/{test_user_id}", headers=headers)
|
||||
if r.status_code != 200:
|
||||
print(f"Failed to get user {test_user_id}: {r.text}")
|
||||
return False
|
||||
|
||||
user_data = r.json()
|
||||
user_id = user_data.get("id")
|
||||
user_email = user_data.get("email")
|
||||
user_role = user_data.get("role")
|
||||
print(f"\nUsing test user: ID={user_id}, Email={user_email}, Role={user_role}")
|
||||
|
||||
# Step 3: Get user details before update
|
||||
print("\n" + "=" * 60)
|
||||
print(f"STEP 3: User details (before update)")
|
||||
print("=" * 60)
|
||||
|
||||
print(f" is_vip: {user_data.get('is_vip')}")
|
||||
print(f" preferred_language: {user_data.get('preferred_language')}")
|
||||
print(f" is_active: {user_data.get('is_active')}")
|
||||
print(f" region_code: {user_data.get('region_code')}")
|
||||
print(f" preferred_currency: {user_data.get('preferred_currency')}")
|
||||
print(f" subscription_plan: {user_data.get('subscription_plan')}")
|
||||
|
||||
# Step 4: PATCH the user
|
||||
print("\n" + "=" * 60)
|
||||
print(f"STEP 4: PATCH user {user_id} - update fields")
|
||||
print("=" * 60)
|
||||
|
||||
patch_data = {
|
||||
"is_vip": True,
|
||||
"preferred_language": "en",
|
||||
"region_code": "HU",
|
||||
"preferred_currency": "HUF",
|
||||
}
|
||||
|
||||
r = await client.patch(f"/admin/users/{user_id}", json=patch_data, headers=headers)
|
||||
print(f"Status: {r.status_code}")
|
||||
|
||||
if r.status_code != 200:
|
||||
print(f"PATCH failed: {r.text}")
|
||||
return False
|
||||
|
||||
updated_user = r.json()
|
||||
print(f"User after update:")
|
||||
print(f" is_vip: {updated_user.get('is_vip')}")
|
||||
print(f" preferred_language: {updated_user.get('preferred_language')}")
|
||||
print(f" region_code: {updated_user.get('region_code')}")
|
||||
print(f" preferred_currency: {updated_user.get('preferred_currency')}")
|
||||
|
||||
# Verify the changes
|
||||
assert updated_user.get("is_vip") == True, "is_vip not updated"
|
||||
assert updated_user.get("preferred_language") == "en", "preferred_language not updated"
|
||||
assert updated_user.get("region_code") == "HU", "region_code not updated"
|
||||
assert updated_user.get("preferred_currency") == "HUF", "preferred_currency not updated"
|
||||
|
||||
print("\n✅ All basic field updates verified!")
|
||||
|
||||
# Step 5: Test person sub-object update
|
||||
print("\n" + "=" * 60)
|
||||
print(f"STEP 5: PATCH user {user_id} - update person fields")
|
||||
print("=" * 60)
|
||||
|
||||
patch_person_data = {
|
||||
"person": {
|
||||
"last_name": "Updated",
|
||||
"first_name": "User",
|
||||
"phone": "+36123456789"
|
||||
}
|
||||
}
|
||||
|
||||
r = await client.patch(f"/admin/users/{user_id}", json=patch_person_data, headers=headers)
|
||||
print(f"Status: {r.status_code}")
|
||||
|
||||
if r.status_code != 200:
|
||||
print(f"PATCH person failed: {r.text}")
|
||||
else:
|
||||
updated_user = r.json()
|
||||
person = updated_user.get("person", {})
|
||||
print(f"Person after update:")
|
||||
print(f" last_name: {person.get('last_name')}")
|
||||
print(f" first_name: {person.get('first_name')}")
|
||||
print(f" phone: {person.get('phone')}")
|
||||
|
||||
if person.get("last_name") == "Updated":
|
||||
print("✅ Person fields updated successfully!")
|
||||
else:
|
||||
print("⚠️ Person fields may not have been updated (might not have a person record)")
|
||||
|
||||
# Step 6: Test error cases
|
||||
print("\n" + "=" * 60)
|
||||
print("STEP 6: Test error cases")
|
||||
print("=" * 60)
|
||||
|
||||
# 6a: Non-existent user
|
||||
r = await client.patch("/admin/users/99999", json={"is_vip": True}, headers=headers)
|
||||
print(f"6a - Non-existent user (99999): Status={r.status_code}")
|
||||
if r.status_code == 404:
|
||||
print("✅ Correctly returned 404")
|
||||
else:
|
||||
print(f"⚠️ Unexpected: {r.text}")
|
||||
|
||||
# 6b: Empty update
|
||||
r = await client.patch(f"/admin/users/{user_id}", json={}, headers=headers)
|
||||
print(f"6b - Empty update: Status={r.status_code}")
|
||||
if r.status_code == 400:
|
||||
print("✅ Correctly returned 400 for empty update")
|
||||
elif r.status_code == 200:
|
||||
print("⚠️ Empty update returned 200 (no changes made)")
|
||||
else:
|
||||
print(f"⚠️ Unexpected: {r.text}")
|
||||
|
||||
# 6c: Invalid field
|
||||
r = await client.patch(f"/admin/users/{user_id}", json={"nonexistent_field": "test"}, headers=headers)
|
||||
print(f"6c - Invalid field: Status={r.status_code}")
|
||||
if r.status_code == 422:
|
||||
print("✅ Correctly returned 422 for invalid field")
|
||||
else:
|
||||
print(f"⚠️ Unexpected: {r.text}")
|
||||
|
||||
# 6d: Try to modify superadmin (should be blocked)
|
||||
r = await client.patch("/admin/users/1", json={"is_vip": True}, headers=headers)
|
||||
print(f"6d - Modify superadmin: Status={r.status_code}")
|
||||
if r.status_code == 403:
|
||||
print("✅ Correctly blocked superadmin modification")
|
||||
else:
|
||||
print(f"⚠️ Unexpected: {r.text}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ALL TESTS COMPLETED")
|
||||
print("=" * 60)
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = asyncio.run(test_admin_user_patch())
|
||||
sys.exit(0 if success else 1)
|
||||
445
backend/tests/test_extract_limit_standalone.py
Normal file
445
backend/tests/test_extract_limit_standalone.py
Normal file
@@ -0,0 +1,445 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
P0 DATA STRUCTURAL AUDIT: _extract_limit() Standalone Test Script
|
||||
|
||||
This script simulates the exact _extract_limit() function and feeds it
|
||||
the ACTUAL JSON data from the database to verify if the limit extraction
|
||||
works correctly for corporate tiers.
|
||||
|
||||
Author: System Architect
|
||||
Date: 2026-06-28
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EXACT copy of _extract_limit() from admin_organizations.py:397
|
||||
# =============================================================================
|
||||
|
||||
def _extract_limit(
|
||||
rules: dict,
|
||||
keys: list[str],
|
||||
default: int = 0,
|
||||
) -> int:
|
||||
"""
|
||||
P0 ARCHITECTURE UNIFICATION: Egyetlen, robusztus függvény a limit értékek
|
||||
kinyerésére a SubscriptionTier.rules JSONB objektumból.
|
||||
|
||||
Két rétegben keres:
|
||||
1. Közvetlenül a rules objektumban: rules["max_vehicles"]
|
||||
2. A rules["allowances"] nested objektumban: rules["allowances"]["max_vehicles"]
|
||||
|
||||
Ez biztosítja, hogy a JSONB struktúra változásai (pl. 'allowances' nested object)
|
||||
ne törjék meg a limit számításokat.
|
||||
|
||||
Args:
|
||||
rules: A SubscriptionTier.rules JSONB dict-je.
|
||||
keys: A keresendő kulcsok listája (pl. ["max_vehicles", "max_assets"]).
|
||||
Az első találat értéke kerül visszaadásra.
|
||||
default: Alapértelmezett érték, ha egyik kulcs sem található.
|
||||
|
||||
Returns:
|
||||
int: A talált limit érték, vagy a default.
|
||||
"""
|
||||
if not rules or not isinstance(rules, dict):
|
||||
return default
|
||||
|
||||
# 1. szint: Közvetlen keresés a rules objektumban
|
||||
for key in keys:
|
||||
value = rules.get(key)
|
||||
if value is not None and isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
|
||||
# 2. szint: Keresés a rules["allowances"] nested objektumban
|
||||
allowances = rules.get("allowances")
|
||||
if allowances and isinstance(allowances, dict):
|
||||
for key in keys:
|
||||
value = allowances.get(key)
|
||||
if value is not None and isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
|
||||
# 3. szint: Keresés a rules["limits"] nested objektumban (alternatív név)
|
||||
limits = rules.get("limits")
|
||||
if limits and isinstance(limits, dict):
|
||||
for key in keys:
|
||||
value = limits.get(key)
|
||||
if value is not None and isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
|
||||
return default
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ACTUAL database JSON data (retrieved via PostgreSQL MCP query on 2026-06-28)
|
||||
# =============================================================================
|
||||
|
||||
# corp_premium_plus_v1 (id=17) — exact JSON as stored in system.subscription_tiers.rules
|
||||
CORP_PREMIUM_PLUS_V1_RAW = """{
|
||||
"type": "corporate",
|
||||
"pricing": {
|
||||
"currency": "EUR",
|
||||
"credit_price": 6000,
|
||||
"yearly_price": 599.99,
|
||||
"monthly_price": 59.99
|
||||
},
|
||||
"duration": {
|
||||
"days": 30,
|
||||
"allow_stacking": true
|
||||
},
|
||||
"ad_policy": {
|
||||
"show_ads": false,
|
||||
"ad_free_grace_days": 0,
|
||||
"max_daily_impressions": null
|
||||
},
|
||||
"affiliate": {
|
||||
"referral_bonus_credits": 250,
|
||||
"commission_rate_percent": 20
|
||||
},
|
||||
"lifecycle": {
|
||||
"is_public": true
|
||||
},
|
||||
"marketing": {
|
||||
"badge": "Plus",
|
||||
"subtitle": "Nagy flották prémium menedzsmentje.",
|
||||
"highlight_color": "#8B5CF6"
|
||||
},
|
||||
"allowances": {
|
||||
"max_garages": 10,
|
||||
"max_vehicles": 50,
|
||||
"monthly_free_credits": 800
|
||||
},
|
||||
"display_name": "Céges Prémium Plus",
|
||||
"entitlements": [
|
||||
"SRV_DATA_EXPORT",
|
||||
"SRV_AI_UPLOAD",
|
||||
"SRV_ACCOUNTING_SYNC"
|
||||
],
|
||||
"pricing_zones": {
|
||||
"HU": {
|
||||
"currency": "HUF",
|
||||
"credit_price": 6000,
|
||||
"yearly_price": 239990,
|
||||
"monthly_price": 23990
|
||||
},
|
||||
"US": {
|
||||
"currency": "USD",
|
||||
"credit_price": 6000,
|
||||
"yearly_price": 699.99,
|
||||
"monthly_price": 69.99
|
||||
},
|
||||
"DEFAULT": {
|
||||
"currency": "EUR",
|
||||
"credit_price": 6000,
|
||||
"yearly_price": 599.99,
|
||||
"monthly_price": 59.99
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
# corp_premium_v1 (id=16) — exact JSON as stored
|
||||
CORP_PREMIUM_V1_RAW = """{
|
||||
"type": "corporate",
|
||||
"pricing": {
|
||||
"currency": "EUR",
|
||||
"credit_price": 3000,
|
||||
"yearly_price": 299.99,
|
||||
"monthly_price": 29.99
|
||||
},
|
||||
"duration": {
|
||||
"days": 30,
|
||||
"allow_stacking": true
|
||||
},
|
||||
"ad_policy": {
|
||||
"show_ads": false,
|
||||
"ad_free_grace_days": 0,
|
||||
"max_daily_impressions": null
|
||||
},
|
||||
"affiliate": {
|
||||
"referral_bonus_credits": 100,
|
||||
"commission_rate_percent": 15
|
||||
},
|
||||
"lifecycle": {
|
||||
"is_public": true
|
||||
},
|
||||
"marketing": {
|
||||
"badge": "Prémium",
|
||||
"subtitle": "Közepes flották számára tervezve.",
|
||||
"highlight_color": "#3B82F6"
|
||||
},
|
||||
"allowances": {
|
||||
"max_garages": 3,
|
||||
"max_vehicles": 20,
|
||||
"monthly_free_credits": 300
|
||||
},
|
||||
"display_name": "Céges Prémium",
|
||||
"entitlements": [
|
||||
"SRV_DATA_EXPORT",
|
||||
"SRV_AI_UPLOAD"
|
||||
],
|
||||
"pricing_zones": {
|
||||
"HU": {
|
||||
"currency": "HUF",
|
||||
"credit_price": 3000,
|
||||
"yearly_price": 119990,
|
||||
"monthly_price": 11990
|
||||
},
|
||||
"US": {
|
||||
"currency": "USD",
|
||||
"credit_price": 3000,
|
||||
"yearly_price": 349.99,
|
||||
"monthly_price": 34.99
|
||||
},
|
||||
"DEFAULT": {
|
||||
"currency": "EUR",
|
||||
"credit_price": 3000,
|
||||
"yearly_price": 299.99,
|
||||
"monthly_price": 29.99
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
# private_pro_v1 (id=14) — exact JSON as stored
|
||||
PRIVATE_PRO_V1_RAW = """{
|
||||
"type": "private",
|
||||
"lifecycle": {
|
||||
"is_public": true,
|
||||
"available_until": null
|
||||
},
|
||||
"marketing": {
|
||||
"badge": null,
|
||||
"subtitle": null
|
||||
},
|
||||
"allowances": {
|
||||
"max_garages": 1,
|
||||
"max_vehicles": 3,
|
||||
"monthly_free_credits": 0
|
||||
},
|
||||
"pricing_zones": {
|
||||
"HU": {
|
||||
"currency": "HUF",
|
||||
"credit_price": 25000,
|
||||
"yearly_price": 9990.0,
|
||||
"monthly_price": 990.0
|
||||
},
|
||||
"US": {
|
||||
"currency": "USD",
|
||||
"credit_price": 25000,
|
||||
"yearly_price": 35.9,
|
||||
"monthly_price": 3.5
|
||||
},
|
||||
"DEFAULT": {
|
||||
"currency": "EUR",
|
||||
"credit_price": 25000,
|
||||
"yearly_price": 29.9,
|
||||
"monthly_price": 2.99
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
# feature_capabilities data (as stored)
|
||||
CORP_PREMIUM_PLUS_V1_FC_RAW = "{}"
|
||||
CORP_PREMIUM_V1_FC_RAW = "{}"
|
||||
PRIVATE_PRO_V1_FC_RAW = """{"export_data": false, "advanced_reports": true, "max_cost_category_depth": 3}"""
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# JSON double-encoding simulation
|
||||
# =============================================================================
|
||||
DOUBLE_ENCODED_RAW = json.dumps(CORP_PREMIUM_PLUS_V1_RAW)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test harness
|
||||
# =============================================================================
|
||||
|
||||
def print_separator(title: str):
|
||||
print(f"\n{'='*80}")
|
||||
print(f" {title}")
|
||||
print(f"{'='*80}")
|
||||
|
||||
|
||||
def test_single_tier(
|
||||
tier_name: str,
|
||||
rules_raw: str,
|
||||
fc_raw: str,
|
||||
vehicle_keys: list,
|
||||
branch_keys: list,
|
||||
user_keys: list,
|
||||
expected_vehicles: int,
|
||||
expected_branches: int,
|
||||
expected_users: int,
|
||||
):
|
||||
"""Test _extract_limit for a single tier, simulating the exact endpoint logic."""
|
||||
print(f"\n ┌─ Tier: {tier_name}")
|
||||
|
||||
rules = json.loads(rules_raw)
|
||||
fc = json.loads(fc_raw)
|
||||
|
||||
print(f" ├─ rules type: {type(rules).__name__}")
|
||||
print(f" ├─ rules keys: {list(rules.keys())}")
|
||||
print(f" ├─ 'allowances' key present: {'allowances' in rules}")
|
||||
if 'allowances' in rules:
|
||||
print(f" ├─ allowances content: {json.dumps(rules['allowances'], indent=4)}")
|
||||
|
||||
base_vehicles = _extract_limit(
|
||||
rules, vehicle_keys,
|
||||
_extract_limit(fc, vehicle_keys, 1),
|
||||
)
|
||||
base_branches = _extract_limit(
|
||||
rules, branch_keys,
|
||||
_extract_limit(fc, branch_keys, 0),
|
||||
)
|
||||
base_users = _extract_limit(
|
||||
rules, user_keys,
|
||||
_extract_limit(fc, user_keys, 0),
|
||||
)
|
||||
|
||||
print(f" ├─ RESULT: vehicles={base_vehicles} (expected={expected_vehicles})")
|
||||
print(f" ├─ RESULT: branches={base_branches} (expected={expected_branches})")
|
||||
print(f" └─ RESULT: users={base_users} (expected={expected_users})")
|
||||
|
||||
status = "✅ PASS" if (base_vehicles == expected_vehicles and
|
||||
base_branches == expected_branches and
|
||||
base_users == expected_users) else "❌ FAIL"
|
||||
print(f" {status}")
|
||||
return status
|
||||
|
||||
|
||||
def test_double_encoding_scenario():
|
||||
"""Test what happens if the JSON is double-encoded."""
|
||||
print_separator("SIMULATION: Double-encoded JSON (a known anti-pattern)")
|
||||
|
||||
double_encoded = DOUBLE_ENCODED_RAW
|
||||
print(f"\n Double-encoded value preview: {double_encoded[:80]}...")
|
||||
print(f" Double-encoded type: {type(double_encoded).__name__}")
|
||||
|
||||
parsed = json.loads(double_encoded)
|
||||
print(f" After json.loads(): type={type(parsed).__name__}")
|
||||
print(f" After json.loads(): is dict? {isinstance(parsed, dict)}")
|
||||
print(f" After json.loads(): is str? {isinstance(parsed, str)}")
|
||||
|
||||
if isinstance(parsed, str):
|
||||
print(f"\n ❌ DOUBLE-ENCODING DETECTED!")
|
||||
print(f" _extract_limit receives a STRING, not a dict!")
|
||||
print(f" First check: 'if not rules or not isinstance(rules, dict):'")
|
||||
print(f" → Returns default=0 immediately!")
|
||||
result = _extract_limit(parsed, ["max_vehicles", "max_assets"], 0)
|
||||
print(f" _extract_limit(string, ...) = {result}")
|
||||
else:
|
||||
print(f"\n ✅ No double-encoding. Data is properly deserialized as dict.")
|
||||
|
||||
|
||||
def test_edge_cases():
|
||||
"""Test edge cases and potential failure modes."""
|
||||
print_separator("EDGE CASE TESTS")
|
||||
|
||||
result = _extract_limit({}, ["max_vehicles"], 5)
|
||||
print(f" Empty dict -> {result} (expected: 5) {'✅' if result == 5 else '❌'}")
|
||||
|
||||
result = _extract_limit(None, ["max_vehicles"], 5)
|
||||
print(f" None -> {result} (expected: 5) {'✅' if result == 5 else '❌'}")
|
||||
|
||||
result = _extract_limit("not a dict", ["max_vehicles"], 5)
|
||||
print(f" String input -> {result} (expected: 5) {'✅' if result == 5 else '❌'}")
|
||||
|
||||
result = _extract_limit({"max_vehicles": 100}, ["max_vehicles", "max_assets"], 0)
|
||||
print(f" Direct key match -> {result} (expected: 100) {'✅' if result == 100 else '❌'}")
|
||||
|
||||
result = _extract_limit({"allowances": {"max_vehicles": 50}}, ["max_vehicles", "max_assets"], 0)
|
||||
print(f" allowances['max_vehicles'] -> {result} (expected: 50) {'✅' if result == 50 else '❌'}")
|
||||
|
||||
result = _extract_limit({"limits": {"max_vehicles": 25}}, ["max_vehicles", "max_assets"], 0)
|
||||
print(f" limits['max_vehicles'] -> {result} (expected: 25) {'✅' if result == 25 else '❌'}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN
|
||||
# =============================================================================
|
||||
|
||||
def main():
|
||||
print_separator("P0 DATA STRUCTURAL AUDIT: _extract_limit() Standalone Test")
|
||||
print(" Date: 2026-06-28")
|
||||
print(" Source: system.subscription_tiers (PostgreSQL JSONB)")
|
||||
|
||||
# --- Test 1: corp_premium_plus_v1 ---
|
||||
print_separator("TEST 1: corp_premium_plus_v1 (id=17)")
|
||||
print(" Expected: vehicles=50, branches=10, users=?")
|
||||
print(" Note: 'max_users'/'max_members' keys NOT present in rules JSON.")
|
||||
print(" The function will fall back to feature_capabilities (empty dict {}),")
|
||||
print(" then to the nested default (0).")
|
||||
|
||||
test_single_tier(
|
||||
tier_name="corp_premium_plus_v1",
|
||||
rules_raw=CORP_PREMIUM_PLUS_V1_RAW,
|
||||
fc_raw=CORP_PREMIUM_PLUS_V1_FC_RAW,
|
||||
vehicle_keys=["max_vehicles", "max_assets"],
|
||||
branch_keys=["max_branches", "max_garages"],
|
||||
user_keys=["max_users", "max_members"],
|
||||
expected_vehicles=50,
|
||||
expected_branches=10,
|
||||
expected_users=0,
|
||||
)
|
||||
|
||||
# --- Test 2: corp_premium_v1 ---
|
||||
print_separator("TEST 2: corp_premium_v1 (id=16)")
|
||||
test_single_tier(
|
||||
tier_name="corp_premium_v1",
|
||||
rules_raw=CORP_PREMIUM_V1_RAW,
|
||||
fc_raw=CORP_PREMIUM_V1_FC_RAW,
|
||||
vehicle_keys=["max_vehicles", "max_assets"],
|
||||
branch_keys=["max_branches", "max_garages"],
|
||||
user_keys=["max_users", "max_members"],
|
||||
expected_vehicles=20,
|
||||
expected_branches=3,
|
||||
expected_users=0,
|
||||
)
|
||||
|
||||
# --- Test 3: private_pro_v1 ---
|
||||
print_separator("TEST 3: private_pro_v1 (id=14)")
|
||||
test_single_tier(
|
||||
tier_name="private_pro_v1",
|
||||
rules_raw=PRIVATE_PRO_V1_RAW,
|
||||
fc_raw=PRIVATE_PRO_V1_FC_RAW,
|
||||
vehicle_keys=["max_vehicles", "max_assets"],
|
||||
branch_keys=["max_branches", "max_garages"],
|
||||
user_keys=["max_users", "max_members"],
|
||||
expected_vehicles=3,
|
||||
expected_branches=1,
|
||||
expected_users=0,
|
||||
)
|
||||
|
||||
# --- Double-encoding simulation ---
|
||||
test_double_encoding_scenario()
|
||||
|
||||
# --- Edge cases ---
|
||||
test_edge_cases()
|
||||
|
||||
# --- Summary ---
|
||||
print_separator("SUMMARY")
|
||||
print("""
|
||||
KEY FINDINGS FROM DB AUDIT:
|
||||
|
||||
1. All corporate tiers (corp_premium_plus_v1, corp_premium_v1)
|
||||
store their limits under rules['allowances'] nested object.
|
||||
|
||||
2. The _extract_limit() function correctly searches at 3 levels:
|
||||
Level 1: Direct keys in root (rules['max_vehicles'])
|
||||
Level 2: Nested under 'allowances' (rules['allowances']['max_vehicles'])
|
||||
Level 3: Nested under 'limits' (rules['limits']['max_vehicles'])
|
||||
|
||||
3. For corp_premium_plus_v1: allowances.max_vehicles = 50
|
||||
Function should return 50, not 0.
|
||||
|
||||
4. ROOT CAUSE SUSPECTED: If _extract_limit returns 0 at runtime,
|
||||
the issue is likely:
|
||||
a) The 'rules' field is double-encoded (string inside JSONB)
|
||||
b) The wrong code path is being executed (e.g., no active_subs found)
|
||||
c) The rules dict is empty {} due to data migration issue
|
||||
""")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
80
docs/admin_users_filter_bug_analysis.md
Normal file
80
docs/admin_users_filter_bug_analysis.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Admin Users List Filter Bug Analysis
|
||||
|
||||
**Gitea Issue:** #323
|
||||
**Scope:** Backend & Frontend (Admin UI)
|
||||
**Type:** Bug
|
||||
|
||||
## Root Cause: Frontend-Backend Parameter Name Mismatch
|
||||
|
||||
The admin users list page at [`frontend_admin/pages/users/index.vue`](frontend_admin/pages/users/index.vue:539) sends filter parameters to the backend endpoint [`GET /api/v1/admin/users`](backend/app/api/v1/endpoints/admin.py:491), but the parameter names don't match what the backend expects.
|
||||
|
||||
### Frontend sends (frontend_admin/pages/users/index.vue, lines 543-556):
|
||||
|
||||
```typescript
|
||||
// What frontend sends:
|
||||
params.status_filter = statusFilter.value // "active", "inactive", "deleted", "banned"
|
||||
params.role_filter = roleFilter.value // "user", "admin", "staff", "superadmin"
|
||||
params.plan_filter = planFilter.value // "FREE", "PREMIUM", "ENTERPRISE"
|
||||
```
|
||||
|
||||
### Backend expects (backend/app/api/v1/endpoints/admin.py, lines 491-656):
|
||||
|
||||
```python
|
||||
# What backend expects:
|
||||
role: Optional[str] = Query(None) # NOT role_filter
|
||||
is_active: Optional[bool] = Query(None) # NOT status_filter
|
||||
is_deleted: Optional[bool] = Query(None) # NOT status_filter
|
||||
# plan_filter: NOT SUPPORTED AT ALL
|
||||
```
|
||||
|
||||
## Detailed Issues
|
||||
|
||||
### Issue 1: Status Filter (`status_filter` vs `is_active`/`is_deleted`)
|
||||
|
||||
| Frontend sends | Should map to backend |
|
||||
|---------------|----------------------|
|
||||
| `status_filter=active` | `is_active=true, is_deleted=false` |
|
||||
| `status_filter=inactive` | `is_active=false, is_deleted=false` |
|
||||
| `status_filter=deleted` | `is_deleted=true` |
|
||||
| `status_filter=banned` | `is_active=false` (no `is_banned` field exists) |
|
||||
|
||||
### Issue 2: Role Filter (`role_filter` vs `role`)
|
||||
|
||||
Simple rename: `role_filter` → `role`
|
||||
|
||||
### Issue 3: Plan Filter (`plan_filter` not supported)
|
||||
|
||||
The backend has **no** `plan_filter` or `subscription_plan` query parameter at all. The `User` model does have a `subscription_plan` column (backend/app/models/identity/identity.py:152), so this needs to be added to the backend.
|
||||
|
||||
### Issue 4: `person_name` missing from response
|
||||
|
||||
The frontend `UserListItem` interface expects `person_name` (line 417), but the backend returns `first_name` and `last_name` separately (lines 640-641). The frontend displays `user.person_name || '—'` (line 246), which means it will always show `—` because `person_name` is never populated.
|
||||
|
||||
## Fix Plan
|
||||
|
||||
### Backend Changes (backend/app/api/v1/endpoints/admin.py)
|
||||
|
||||
1. **Add `plan_filter` query parameter** to the `list_users` endpoint:
|
||||
- Accept `plan_filter: Optional[str]`
|
||||
- Filter by `User.subscription_plan == plan_filter`
|
||||
- Valid values: "FREE", "PREMIUM", "ENTERPRISE" (plus "all")
|
||||
|
||||
2. **Add `person_name` to the response** by concatenating `first_name` and `last_name`.
|
||||
|
||||
### Frontend Changes (frontend_admin/pages/users/index.vue)
|
||||
|
||||
1. **Rename parameter**: Change `params.role_filter` → `params.role`
|
||||
2. **Status filter logic**: Replace `params.status_filter` with proper mapping:
|
||||
- `active` → `params.is_active = true`
|
||||
- `inactive` → `params.is_active = false`
|
||||
- `deleted` → `params.is_deleted = true`
|
||||
- `banned` → `params.is_active = false` (since banned users are deactivated)
|
||||
3. **Keep `plan_filter`** as-is (backend will support it after fix)
|
||||
4. **Fix `person_name`** display: Compute it from `first_name` + `last_name` if available
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `backend/app/api/v1/endpoints/admin.py` | Add `plan_filter` param, add `person_name` to response |
|
||||
| `frontend_admin/pages/users/index.vue` | Fix parameter names and status mapping in `fetchUsers()` |
|
||||
189
docs/p0_subscription_jsonb_structural_audit_report.md
Normal file
189
docs/p0_subscription_jsonb_structural_audit_report.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# P0 DATA STRUCTURAL AUDIT REPORT: `_extract_limit()` & JSONB Tier Structure
|
||||
|
||||
**Date:** 2026-06-28
|
||||
**Author:** System Architect
|
||||
**Scope:** [`system.subscription_tiers`](backend/app/models/core_logic.py:68), [`finance.org_subscriptions`](backend/app/models/core_logic.py:143), [`_extract_limit()`](backend/app/api/v1/endpoints/admin_organizations.py:397)
|
||||
**Status:** ✅ Structural audit complete — `_extract_limit` logic is **correct**, DB JSONB is **valid**
|
||||
|
||||
---
|
||||
|
||||
## 1. AUDIT: `system.subscription_tiers` (Base Tier Rules)
|
||||
|
||||
### Raw JSON Structure
|
||||
|
||||
All three corporate tiers have their limits stored at **`rules["allowances"]`** (nested object, 2nd level):
|
||||
|
||||
| Tier | ID | `jsonb_typeof(rules)` | `allowances.max_vehicles` | `allowances.max_garages` |
|
||||
|------|----|----------------------|--------------------------|--------------------------|
|
||||
| `private_pro_v1` | 14 | `object` ✅ | 3 | 1 |
|
||||
| `corp_premium_v1` | 16 | `object` ✅ | 20 | 3 |
|
||||
| `corp_premium_plus_v1` | 17 | `object` ✅ | 50 | 10 |
|
||||
|
||||
### Full JSON for `corp_premium_plus_v1` (id=17)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "corporate",
|
||||
"pricing": { "currency": "EUR", "credit_price": 6000, "yearly_price": 599.99, "monthly_price": 59.99 },
|
||||
"duration": { "days": 30, "allow_stacking": true },
|
||||
"ad_policy": { "show_ads": false, "ad_free_grace_days": 0, "max_daily_impressions": null },
|
||||
"affiliate": { "referral_bonus_credits": 250, "commission_rate_percent": 20 },
|
||||
"lifecycle": { "is_public": true },
|
||||
"marketing": { "badge": "Plus", "subtitle": "Nagy flották prémium menedzsmentje.", "highlight_color": "#8B5CF6" },
|
||||
"allowances": { "max_garages": 10, "max_vehicles": 50, "monthly_free_credits": 800 },
|
||||
"display_name": "Ceiges Preimium Plus",
|
||||
"entitlements": ["SRV_DATA_EXPORT", "SRV_AI_UPLOAD", "SRV_ACCOUNTING_SYNC"],
|
||||
"pricing_zones": { "HU": { "currency": "HUF", "credit_price": 6000, "yearly_price": 239990, "monthly_price": 23990 }, "DEFAULT": { "currency": "EUR", "credit_price": 6000, "yearly_price": 599.99, "monthly_price": 59.99 } }
|
||||
}
|
||||
```
|
||||
|
||||
### Confirmed: `rules` is a proper JSONB **object**, NOT a string
|
||||
|
||||
```sql
|
||||
SELECT jsonb_typeof(rules) FROM system.subscription_tiers WHERE id = 17;
|
||||
-- Result: "object" ✅ (not "string")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. AUDIT: `finance.org_subscriptions` (Active Subs & Extra Allowances)
|
||||
|
||||
| org_id | Sub ID | Tier | Tier ID | `extra_allowances` |
|
||||
|--------|--------|------|---------|-------------------|
|
||||
| 15 | 2 | `corp_premium_v1` | 16 | (not queried) |
|
||||
| 43 | 30 | `private_pro_v1` | 14 | `{}` |
|
||||
| 44 | 22 | `corp_premium_v1` | 16 | (not queried) |
|
||||
| 45 | 21 | `corp_premium_plus_v1` | 17 | (not queried) |
|
||||
| 49 | 20 | `corp_premium_v1` | 16 | (not queried) |
|
||||
|
||||
For org_id=43: `extra_allowances` = `{}` (empty JSONB object) — no extra allowances purchased.
|
||||
|
||||
---
|
||||
|
||||
## 3. TEST SCRIPT: `_extract_limit()` Behavior
|
||||
|
||||
### Implementation (exact copy from [`admin_organizations.py:397`](backend/app/api/v1/endpoints/admin_organizations.py:397))
|
||||
|
||||
The function searches at **3 levels**:
|
||||
1. **Level 1**: Direct keys at rules root (`rules["max_vehicles"]`)
|
||||
2. **Level 2**: Nested under `rules["allowances"]` (`rules["allowances"]["max_vehicles"]`)
|
||||
3. **Level 3**: Nested under `rules["limits"]` (`rules["limits"]["max_vehicles"]`)
|
||||
|
||||
### Test Results — ALL PASS ✅
|
||||
|
||||
| Tier | Vehicles | Branches | Users | Status |
|
||||
|------|----------|----------|-------|--------|
|
||||
| `corp_premium_plus_v1` | **50** ✅ | **10** ✅ | 0 | **PASS** |
|
||||
| `corp_premium_v1` | **20** ✅ | **3** ✅ | 0 | **PASS** |
|
||||
| `private_pro_v1` | **3** ✅ | **1** ✅ | 0 | **PASS** |
|
||||
|
||||
### Edge Cases
|
||||
|
||||
| Input | Result | Expected | Status |
|
||||
|-------|--------|----------|--------|
|
||||
| Empty dict `{}` | 5 (default) | 5 | ✅ |
|
||||
| `None` | 5 (default) | 5 | ✅ |
|
||||
| String (not dict) | 5 (default) | 5 | ✅ |
|
||||
| Direct key match | 100 | 100 | ✅ |
|
||||
| `allowances.max_vehicles` | 50 | 50 | ✅ |
|
||||
| `limits.max_vehicles` | 25 | 25 | ✅ |
|
||||
|
||||
### Double-Encoding Simulation
|
||||
|
||||
When the JSON is **double-encoded** (string stored inside JSONB instead of object):
|
||||
|
||||
```
|
||||
Input: '"{\\"allowances\\": {\\"max_vehicles\\": 50}}"'
|
||||
→ json.loads() returns str, not dict
|
||||
→ _extract_limit(str, ...) returns default=0 ❌
|
||||
```
|
||||
|
||||
✅ **Confirmed: Current DB data is NOT double-encoded.** (`jsonb_typeof = "object"`)
|
||||
|
||||
---
|
||||
|
||||
## 4. ROOT CAUSE ANALYSIS
|
||||
|
||||
### The `_extract_limit()` function is **NOT the problem**
|
||||
|
||||
All tests confirm the function correctly extracts limits from the current JSONB structure. It fully supports the `allowances` nested format used by all tiers.
|
||||
|
||||
### The REAL problem is likely in the **code flow** before `_extract_limit` is called
|
||||
|
||||
The endpoint [`get_organization_details()`](backend/app/api/v1/endpoints/admin_organizations.py:462) has **two code paths**:
|
||||
|
||||
#### Path A (line 579-664) — Uses `finance.org_subscriptions` (NEW system)
|
||||
|
||||
```python
|
||||
active_subs = sub_result.scalars().all() # Query finance.org_subscriptions
|
||||
if active_subs:
|
||||
base_subs = [s for s in active_subs if s.tier and s.tier.type in (None, '', 'base')]
|
||||
primary_sub = base_subs_sorted[0] if base_subs_sorted else ...
|
||||
if primary_sub and primary_sub.tier:
|
||||
primary_rules = primary_sub.tier.rules or {} # ← THIS must work
|
||||
```
|
||||
|
||||
#### Path B (line 665-691) — Uses `fleet.organizations.subscription_tier_id` (LEGACY)
|
||||
|
||||
```python
|
||||
elif org.subscription_tier:
|
||||
rules = org.subscription_tier.rules or {} # ← LEGACY path
|
||||
```
|
||||
|
||||
### Potential failure scenarios (outside `_extract_limit`):
|
||||
|
||||
| Scenario | Symptom | Likelihood |
|
||||
|----------|---------|------------|
|
||||
| **Path A → `active_subs` is empty** (no `finance.org_subscriptions` row) | Falls to Path B | Medium |
|
||||
| **Path A → `primary_sub.tier` is `None`** (FK broken, tier missing) | Skips limit calc, `subscription_summary` stays `None` | Low |
|
||||
| **Path B → `org.subscription_tier` points to wrong tier** (old FK mismatch) | Wrong limits returned | Medium |
|
||||
| **`org.subscription_tier.rules` is `None`** (tier exists but rules column is NULL) | `{} or None` → `{}` → default | Low |
|
||||
|
||||
---
|
||||
|
||||
## 5. RECOMMENDATIONS
|
||||
|
||||
### ✅ Short-term: Verify runtime dataflow
|
||||
|
||||
1. **Test with exact org_id** that shows the 0 limit:
|
||||
```sql
|
||||
SELECT os.id, os.org_id, os.tier_id, st.name, st.rules IS NOT NULL as has_rules
|
||||
FROM finance.org_subscriptions os
|
||||
JOIN system.subscription_tiers st ON os.tier_id = st.id
|
||||
WHERE os.org_id = <problematic_org_id> AND os.is_active = true;
|
||||
```
|
||||
|
||||
2. **Check if the endpoint takes Path A or Path B** by inspecting logs/Debug mode.
|
||||
|
||||
### ✅ Long-term: Code improvements (separate ticket)
|
||||
|
||||
1. **Add logging** at key decision points in [`get_organization_details()`](backend/app/api/v1/endpoints/admin_organizations.py:579-614):
|
||||
- Log `active_subs` count
|
||||
- Log which path is taken (A vs B)
|
||||
- Log `primary_rules` keys and extracted limits
|
||||
- Log if `primary_sub.tier` is None
|
||||
|
||||
2. **Add defensive fallback** — if `_extract_limit` returns 0 but `rules` has `allowances`, log a warning:
|
||||
```python
|
||||
base_vehicles = _extract_limit(primary_rules, ["max_vehicles", "max_assets"], fc_fallback)
|
||||
if base_vehicles == 0 and primary_rules.get("allowances", {}).get("max_vehicles"):
|
||||
logger.warning(f"_extract_limit returned 0 despite allowances.max_vehicles={primary_rules['allowances']['max_vehicles']}")
|
||||
```
|
||||
|
||||
### 🔍 Most Probable Root Cause
|
||||
|
||||
The issue is likely that **the org(s) reporting 0 limits do NOT have an active row in `finance.org_subscriptions`** (Path A), and the **legacy Path B is failing** because `org.subscription_tier.rules` is `None` or the old FK points to a tier that doesn't have `allowances` populated.
|
||||
|
||||
**Next step**: Identify the exact org_id(s) that show 0 and check their `finance.org_subscriptions` presence and `fleet.organizations.subscription_tier_id` value.
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Test Script Location
|
||||
|
||||
The standalone test script is at:
|
||||
[`tests/active/test_extract_limit_standalone.py`](tests/active/test_extract_limit_standalone.py)
|
||||
|
||||
Run with:
|
||||
```bash
|
||||
docker compose exec sf_api python3 /app/tests/test_extract_limit_standalone.py
|
||||
```
|
||||
259
docs/p0_subscription_math_trace_report.md
Normal file
259
docs/p0_subscription_math_trace_report.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# P0 DEEP DIVE DIAGNOSTIC — Subscription Math Trace Report
|
||||
|
||||
**Date:** 2026-06-27
|
||||
**Author:** Fast Coder (Core Developer)
|
||||
**Scope:** Root cause analysis of `asset_limit` returning 0 for `corp_premium_plus_v1` tier
|
||||
|
||||
---
|
||||
|
||||
## 1. DATABASE DUMP — Raw JSONB for `corp_premium_plus_v1`
|
||||
|
||||
### Query
|
||||
```sql
|
||||
SELECT id, name, rules, feature_capabilities
|
||||
FROM system.subscription_tiers
|
||||
WHERE name = 'corp_premium_plus_v1';
|
||||
```
|
||||
|
||||
### Result
|
||||
|
||||
| Column | Value |
|
||||
|--------|-------|
|
||||
| `id` | `17` |
|
||||
| `name` | `corp_premium_plus_v1` |
|
||||
| `rules` (JSONB) | See below |
|
||||
| `feature_capabilities` | `{}` (empty object) |
|
||||
|
||||
### Raw `rules` JSONB (pretty-printed)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "corporate",
|
||||
"pricing": {
|
||||
"currency": "EUR",
|
||||
"credit_price": 6000,
|
||||
"yearly_price": 599.99,
|
||||
"monthly_price": 59.99
|
||||
},
|
||||
"duration": {
|
||||
"days": 30,
|
||||
"allow_stacking": true
|
||||
},
|
||||
"ad_policy": {
|
||||
"show_ads": false,
|
||||
"ad_free_grace_days": 0,
|
||||
"max_daily_impressions": null
|
||||
},
|
||||
"affiliate": {
|
||||
"referral_bonus_credits": 250,
|
||||
"commission_rate_percent": 20
|
||||
},
|
||||
"lifecycle": {
|
||||
"is_public": true
|
||||
},
|
||||
"marketing": {
|
||||
"badge": "Plus",
|
||||
"subtitle": "Nagy flották prémium menedzsmentje.",
|
||||
"highlight_color": "#8B5CF6"
|
||||
},
|
||||
"allowances": {
|
||||
"max_garages": 10,
|
||||
"max_vehicles": 50,
|
||||
"monthly_free_credits": 800
|
||||
},
|
||||
"display_name": "Céges Prémium Plus",
|
||||
"entitlements": [
|
||||
"SRV_DATA_EXPORT",
|
||||
"SRV_AI_UPLOAD",
|
||||
"SRV_ACCOUNTING_SYNC"
|
||||
],
|
||||
"pricing_zones": {
|
||||
"HU": { "currency": "HUF", "credit_price": 6000, "yearly_price": 239990, "monthly_price": 23990 },
|
||||
"US": { "currency": "USD", "credit_price": 6000, "yearly_price": 699.99, "monthly_price": 69.99 },
|
||||
"DEFAULT": { "currency": "EUR", "credit_price": 6000, "yearly_price": 599.99, "monthly_price": 59.99 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Observation
|
||||
|
||||
The vehicle limit **`max_vehicles: 50`** is stored **inside a nested `allowances` object** within the `rules` JSONB column:
|
||||
|
||||
```
|
||||
rules → allowances → max_vehicles: 50
|
||||
```
|
||||
|
||||
The `feature_capabilities` column is **empty** (`{}`).
|
||||
|
||||
---
|
||||
|
||||
## 2. MATH TRACE — The Aggregation Logic
|
||||
|
||||
### File: [`backend/app/api/v1/endpoints/admin_organizations.py`](backend/app/api/v1/endpoints/admin_organizations.py:600)
|
||||
|
||||
### The Formula (line 680)
|
||||
|
||||
```python
|
||||
asset_limit = base_vehicles + extra_vehicles + addon_vehicles
|
||||
```
|
||||
|
||||
Where:
|
||||
- `base_vehicles` ← extracted from the **base tier's** `rules` + `feature_capabilities` + `extra_allowances`
|
||||
- `extra_vehicles` ← from `base_extra.get("extra_vehicles", 0)` (admin overrides in `OrganizationSubscription.extra_allowances`)
|
||||
- `addon_vehicles` ← summed across all active add-on subscriptions
|
||||
|
||||
### The `_extract_limit` Function (lines 601–630)
|
||||
|
||||
```python
|
||||
def _extract_limit(
|
||||
rules: dict,
|
||||
fc: dict,
|
||||
extra: dict,
|
||||
vehicle_keys: tuple = ("max_vehicles", "vehicle_limit", "max_assets", "assets_limit"),
|
||||
branch_keys: tuple = ("max_branches", "branch_limit", "max_garages", "garages_limit"),
|
||||
user_keys: tuple = ("max_users", "user_limit", "max_members", "members_limit"),
|
||||
default: int = 0,
|
||||
):
|
||||
def _pick(keys: tuple) -> int:
|
||||
# 1. Check root level of rules, fc, extra
|
||||
for d in (rules, fc, extra):
|
||||
for k in keys:
|
||||
val = d.get(k)
|
||||
if val is not None and isinstance(val, (int, float)):
|
||||
return int(val)
|
||||
# 2. Check nested 'allowances' object inside rules
|
||||
allowances = rules.get("allowances", {})
|
||||
if isinstance(allowances, dict):
|
||||
for k in keys:
|
||||
val = allowances.get(k)
|
||||
if val is not None and isinstance(val, (int, float)):
|
||||
return int(val)
|
||||
return default
|
||||
return _pick(vehicle_keys), _pick(branch_keys), _pick(user_keys)
|
||||
```
|
||||
|
||||
### How It's Called (line 648)
|
||||
|
||||
```python
|
||||
base_vehicles, base_branches, base_users = _extract_limit(rules, fc, base_extra)
|
||||
```
|
||||
|
||||
Where:
|
||||
- `rules` = `active_base_sub.tier.rules` (the JSONB shown above)
|
||||
- `fc` = `active_base_sub.tier.feature_capabilities` → `{}` (empty)
|
||||
- `base_extra` = `active_base_sub.extra_allowances` → `{}` (empty, unless admin set overrides)
|
||||
|
||||
---
|
||||
|
||||
## 3. ROOT CAUSE ANALYSIS
|
||||
|
||||
### Step-by-step trace of `_pick(vehicle_keys)`:
|
||||
|
||||
**Phase 1 — Root level scan of `rules`, `fc`, `extra`:**
|
||||
|
||||
The function iterates over `(rules, fc, extra)` and checks each key in `vehicle_keys = ("max_vehicles", "vehicle_limit", "max_assets", "assets_limit")`.
|
||||
|
||||
| Dictionary | `max_vehicles` | `vehicle_limit` | `max_assets` | `assets_limit` |
|
||||
|------------|---------------|-----------------|--------------|----------------|
|
||||
| `rules` (root level) | ❌ Not found | ❌ Not found | ❌ Not found | ❌ Not found |
|
||||
| `fc` = `{}` | ❌ Not found | ❌ Not found | ❌ Not found | ❌ Not found |
|
||||
| `extra` = `{}` | ❌ Not found | ❌ Not found | ❌ Not found | ❌ Not found |
|
||||
|
||||
**Result: `None` from all three dictionaries.**
|
||||
|
||||
**Phase 2 — Nested `allowances` scan:**
|
||||
|
||||
```python
|
||||
allowances = rules.get("allowances", {}) # → {"max_garages": 10, "max_vehicles": 50, "monthly_free_credits": 800}
|
||||
```
|
||||
|
||||
Now it checks `allowances.get("max_vehicles")` → **`50`** ✓
|
||||
|
||||
**This SHOULD work!** The nested `allowances` fallback was added as a "P0 JSONB NESTING HOTFIX" (see comment on line 612).
|
||||
|
||||
### The Real Problem: TWO CODE PATHS
|
||||
|
||||
The `get_organization_details` function has **two separate code paths** for computing the subscription summary:
|
||||
|
||||
#### Path A: `active_sub` exists (line 519–550) — **OLD CODE**
|
||||
```python
|
||||
if active_sub and active_sub.tier:
|
||||
rules = active_sub.tier.rules or {}
|
||||
fc = active_sub.tier.feature_capabilities or {}
|
||||
extra = active_sub.extra_allowances or {}
|
||||
|
||||
# Base limits from tier rules (with explicit .get() — no `or` bug)
|
||||
base_vehicles = rules.get("max_vehicles", fc.get("max_vehicles", 1))
|
||||
base_branches = rules.get("max_branches", fc.get("max_branches", 0))
|
||||
base_users = rules.get("max_users", fc.get("max_users", 0))
|
||||
```
|
||||
|
||||
**BUG:** This path uses `rules.get("max_vehicles", ...)` directly — it does **NOT** check the nested `allowances` object! Since `max_vehicles` is at `rules → allowances → max_vehicles`, not at `rules → max_vehicles`, this returns the fallback value of `1`.
|
||||
|
||||
#### Path B: Multi-subscription aggregation (line 600–699) — **NEW CODE with `_extract_limit`**
|
||||
```python
|
||||
if active_base_sub and active_base_sub.tier:
|
||||
rules = active_base_sub.tier.rules or {}
|
||||
fc = active_base_sub.tier.feature_capabilities or {}
|
||||
base_extra = active_base_sub.extra_allowances or {}
|
||||
|
||||
base_vehicles, base_branches, base_users = _extract_limit(rules, fc, base_extra)
|
||||
```
|
||||
|
||||
**This path DOES work correctly** because `_extract_limit` checks the nested `allowances` object.
|
||||
|
||||
### Which path is being hit?
|
||||
|
||||
Looking at the subscription query (lines 443–455):
|
||||
|
||||
```python
|
||||
sub_stmt = (
|
||||
select(OrganizationSubscription)
|
||||
.options(selectinload(OrganizationSubscription.tier))
|
||||
.where(
|
||||
OrganizationSubscription.org_id == org_id,
|
||||
OrganizationSubscription.is_active == True,
|
||||
OrganizationSubscription.valid_until >= now,
|
||||
)
|
||||
.order_by(OrganizationSubscription.id.desc())
|
||||
.limit(1)
|
||||
)
|
||||
sub_result = await db.execute(sub_stmt)
|
||||
active_sub = sub_result.scalar_one_or_none()
|
||||
```
|
||||
|
||||
This query returns a **single** subscription row (`.limit(1)`). If the org has a `finance.org_subscriptions` row with `is_active=True` and `valid_until >= now`, then `active_sub` is set and **Path A** is executed — which has the bug.
|
||||
|
||||
**Path B** (the multi-subscription aggregation with `_extract_limit`) is only reached if the subscription query returns NO rows (`active_sub` is None), which would be the case if the org has no subscription record.
|
||||
|
||||
### Summary of the Bug
|
||||
|
||||
| Aspect | Detail |
|
||||
|--------|--------|
|
||||
| **Bug Location** | [`admin_organizations.py:525`](backend/app/api/v1/endpoints/admin_organizations.py:525) — Path A |
|
||||
| **Root Cause** | `rules.get("max_vehicles", ...)` only checks the **root level** of the `rules` JSONB. The actual value is nested inside `rules → allowances → max_vehicles`. |
|
||||
| **Affected Data** | All tiers that store limits inside a nested `allowances` object (all tiers in the DB use this structure) |
|
||||
| **Why it returns 0** | It doesn't return 0 — it returns `1` (the fallback from `fc.get("max_vehicles", 1)`). But the **Total Limit** equals the **Extra Limit** because the base calculation returns only the fallback `1` instead of the real `50`. |
|
||||
| **Fix** | Path A needs to also check the nested `allowances` object, just like `_extract_limit` does in Path B. |
|
||||
|
||||
---
|
||||
|
||||
## 4. COMPARISON: `evidence.py` Works Correctly
|
||||
|
||||
Interestingly, the [`evidence.py`](backend/app/api/v1/endpoints/evidence.py:36) endpoint already handles this correctly:
|
||||
|
||||
```python
|
||||
if tier and tier.rules:
|
||||
max_vehicles = tier.rules.get("allowances", {}).get("max_vehicles", 1)
|
||||
max_allowed = max(int(max_vehicles), 1)
|
||||
```
|
||||
|
||||
It directly accesses `rules → allowances → max_vehicles` with the nested `.get("allowances", {}).get("max_vehicles", 1)` pattern.
|
||||
|
||||
---
|
||||
|
||||
## 5. CONCLUSION
|
||||
|
||||
The `_extract_limit` function (Path B) is **correct** — it properly handles the nested `allowances` structure. However, the **old Path A** (lines 519–550) is still active and is the one being hit when the org has a valid `finance.org_subscriptions` row. Path A uses `rules.get("max_vehicles", ...)` which only checks the root level of the JSONB, missing the nested `allowances` object.
|
||||
|
||||
**The fix is to update Path A** (lines 525–527) to also search inside the nested `allowances` object, or to refactor Path A to use the same `_extract_limit` helper that Path B uses.
|
||||
8
frontend_admin/.nuxt/components.d.ts
vendored
8
frontend_admin/.nuxt/components.d.ts
vendored
@@ -14,6 +14,10 @@ type HydrationStrategies = {
|
||||
type LazyComponent<T> = DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T
|
||||
|
||||
|
||||
export const ChartsUserTrendChart: typeof import("../components/charts/UserTrendChart.vue")['default']
|
||||
export const GaragesGarageFleetTab: typeof import("../components/garages/GarageFleetTab.vue")['default']
|
||||
export const GaragesGarageGeneralTab: typeof import("../components/garages/GarageGeneralTab.vue")['default']
|
||||
export const GaragesGarageSubscriptionTab: typeof import("../components/garages/GarageSubscriptionTab.vue")['default']
|
||||
export const NuxtWelcome: typeof import("../node_modules/nuxt/dist/app/components/welcome.vue")['default']
|
||||
export const NuxtLayout: typeof import("../node_modules/nuxt/dist/app/components/nuxt-layout")['default']
|
||||
export const NuxtErrorBoundary: typeof import("../node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue")['default']
|
||||
@@ -39,6 +43,10 @@ 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 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 LazyChartsUserTrendChart: LazyComponent<typeof import("../components/charts/UserTrendChart.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 LazyGaragesGarageSubscriptionTab: LazyComponent<typeof import("../components/garages/GarageSubscriptionTab.vue")['default']>
|
||||
export const LazyNuxtWelcome: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/welcome.vue")['default']>
|
||||
export const LazyNuxtLayout: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/nuxt-layout")['default']>
|
||||
export const LazyNuxtErrorBoundary: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue")['default']>
|
||||
|
||||
@@ -2311,6 +2311,7 @@ var packages$1 = {
|
||||
allowances: "Korlátok",
|
||||
max_vehicles: "Max Jármű",
|
||||
max_garages: "Max Garázs",
|
||||
max_users: "Max Dolgozó",
|
||||
monthly_credits: "Havi Kredit",
|
||||
tier_level: "Szint",
|
||||
form_name: "Rendszer Név",
|
||||
@@ -2389,6 +2390,16 @@ var garages$1 = {
|
||||
subscription_updated: "előfizetése sikeresen frissítve!",
|
||||
view_details: "Részletek",
|
||||
indefinite: "Határozatlan",
|
||||
addons_title: "Kiegészítők (Add-ons)",
|
||||
addon_vehicles: "Extra Jármű",
|
||||
addon_branches: "Extra Telephely",
|
||||
addon_users: "Extra Dolgozó",
|
||||
active_addons: "Aktív kiegészítők",
|
||||
no_active_addons: "Nincsenek aktív kiegészítők",
|
||||
addon_vehicles_expiration: "Extra Jármű lejárati dátuma",
|
||||
addon_branches_expiration: "Extra Telephely lejárati dátuma",
|
||||
addon_users_expiration: "Extra Dolgozó lejárati dátuma",
|
||||
addon_expiration_hint: "Ha üres, a meglévő lejárati dátumok megmaradnak.",
|
||||
details: {
|
||||
loading: "Garázs adatainak betöltése...",
|
||||
retry: "Újra",
|
||||
@@ -2462,6 +2473,8 @@ var garages$1 = {
|
||||
vehicles_used: "Járművek",
|
||||
branches_used: "Telephelyek",
|
||||
employees_used: "Dolgozók",
|
||||
branches: "telephely",
|
||||
employees: "dolgozó",
|
||||
status: "Státusz",
|
||||
subscription_history: "Előfizetési Előzmények",
|
||||
history_date: "Dátum",
|
||||
@@ -2545,10 +2558,109 @@ var garages$1 = {
|
||||
no_fleet_access: "Nincs jogosultságod a flotta adatok megtekintéséhez."
|
||||
}
|
||||
};
|
||||
var users$1 = {
|
||||
title: "Felhasználó Kezelés",
|
||||
subtitle: "Felhasználók listája, keresés, szűrés és tömeges műveletek",
|
||||
loading: "Felhasználók betöltése...",
|
||||
retry: "Újra",
|
||||
search_placeholder: "Keresés email, név vagy telefonszám alapján...",
|
||||
all_statuses: "Összes státusz",
|
||||
status_active: "Aktív",
|
||||
status_inactive: "Inaktív",
|
||||
status_deleted: "Törölt",
|
||||
status_banned: "Kitiltott",
|
||||
all_roles: "Összes szerepkör",
|
||||
role_user: "Felhasználó",
|
||||
role_admin: "Admin",
|
||||
role_staff: "Munkatárs",
|
||||
role_superadmin: "Superadmin",
|
||||
all_plans: "Összes csomag",
|
||||
plan_free: "Ingyenes",
|
||||
plan_premium: "Prémium",
|
||||
plan_enterprise: "Vállalati",
|
||||
total_users: "Összes Felhasználó",
|
||||
active_users: "Aktív Felhasználók",
|
||||
deleted_users: "Törölt Felhasználók",
|
||||
banned_users: "Kitiltott Felhasználók",
|
||||
new_today: "Ma Regisztrált",
|
||||
filtered_results: "Szűrt találat",
|
||||
selected_count: "{count} felhasználó kiválasztva",
|
||||
bulk_activate: "Aktiválás",
|
||||
bulk_deactivate: "Deaktiválás",
|
||||
clear_selection: "Kijelölés törlése",
|
||||
email: "E-mail",
|
||||
name: "Név",
|
||||
role: "Szerepkör",
|
||||
status: "Státusz",
|
||||
"package": "Csomag",
|
||||
registration: "Regisztráció",
|
||||
language: "Nyelv",
|
||||
actions: "Műveletek",
|
||||
view: "Megtekintés",
|
||||
activate: "Aktiválás",
|
||||
deactivate: "Deaktiválás",
|
||||
no_results: "Nincs a keresésnek megfelelő felhasználó.",
|
||||
showing: "Mutató",
|
||||
prev: "Előző",
|
||||
next: "Következő",
|
||||
details: {
|
||||
loading: "Felhasználó adatainak betöltése...",
|
||||
retry: "Újra",
|
||||
back: "Vissza a felhasználókhoz",
|
||||
edit: "Felhasználó szerkesztése",
|
||||
edit_user: "Felhasználó szerkesztése",
|
||||
profile_tab: "Profil",
|
||||
memberships_tab: "Tagságok",
|
||||
account_info: "Fiók Információk",
|
||||
user_id: "Felhasználó ID",
|
||||
registration_date: "Regisztráció Dátuma",
|
||||
last_login: "Utolsó Belépés",
|
||||
ui_mode: "UI Mód",
|
||||
region: "Régió",
|
||||
currency: "Pénznem",
|
||||
subscription_info: "Előfizetés Információk",
|
||||
subscription_expires: "Lejárat Dátuma",
|
||||
scope_level: "Hatáskör Szint",
|
||||
scope_id: "Hatáskör ID",
|
||||
max_vehicles: "Max Járművek",
|
||||
max_garages: "Max Garázsok",
|
||||
personal_info: "Személyes Információk",
|
||||
last_name: "Vezetéknév",
|
||||
first_name: "Keresztnév",
|
||||
phone: "Telefonszám",
|
||||
birth_place: "Születési Hely",
|
||||
birth_date: "Születési Dátum",
|
||||
mothers_name: "Anyja Vezetékneve",
|
||||
mothers_first_name: "Anyja Keresztneve",
|
||||
address: "Cím",
|
||||
zip: "Irányítószám",
|
||||
city: "Város",
|
||||
street: "Utca",
|
||||
house_number: "Házszám",
|
||||
no_person_record: "Nem található személyes adat ehhez a felhasználóhoz.",
|
||||
memberships: "Szervezeti Tagságok",
|
||||
memberships_count: "{count} tagság",
|
||||
no_memberships: "Ez a felhasználó nem tagja egyetlen szervezetnek sem.",
|
||||
org_name: "Szervezet",
|
||||
org_role: "Szerepkör",
|
||||
member_status: "Státusz",
|
||||
joined_at: "Csatlakozott",
|
||||
verified: "Ellenőrzött",
|
||||
verified_yes: "Igen",
|
||||
verified_no: "Nem",
|
||||
preferred_language: "Előnyben Részesített Nyelv",
|
||||
is_active: "Aktív",
|
||||
is_vip: "VIP",
|
||||
cancel: "Mégse",
|
||||
save: "Változtatások Mentése",
|
||||
saving: "Mentés..."
|
||||
}
|
||||
};
|
||||
const locale_hu_46json_ee06c965 = {
|
||||
permissions: permissions$1,
|
||||
packages: packages$1,
|
||||
garages: garages$1
|
||||
garages: garages$1,
|
||||
users: users$1
|
||||
};
|
||||
|
||||
var permissions = {
|
||||
@@ -2610,6 +2722,7 @@ var packages = {
|
||||
allowances: "Allowances",
|
||||
max_vehicles: "Max Vehicles",
|
||||
max_garages: "Max Garages",
|
||||
max_users: "Max Users",
|
||||
monthly_credits: "Monthly Credits",
|
||||
tier_level: "Level",
|
||||
form_name: "System Name",
|
||||
@@ -2688,6 +2801,16 @@ var garages = {
|
||||
subscription_updated: "subscription updated successfully!",
|
||||
view_details: "Details",
|
||||
indefinite: "Indefinite",
|
||||
addons_title: "Add-ons",
|
||||
addon_vehicles: "Extra Vehicle",
|
||||
addon_branches: "Extra Branch",
|
||||
addon_users: "Extra Employee",
|
||||
active_addons: "Active Add-ons",
|
||||
no_active_addons: "No active add-ons",
|
||||
addon_vehicles_expiration: "Extra Vehicle Expiration",
|
||||
addon_branches_expiration: "Extra Branch Expiration",
|
||||
addon_users_expiration: "Extra Employee Expiration",
|
||||
addon_expiration_hint: "If empty, existing expiration dates remain.",
|
||||
details: {
|
||||
loading: "Loading garage details...",
|
||||
retry: "Retry",
|
||||
@@ -2761,6 +2884,8 @@ var garages = {
|
||||
vehicles_used: "Vehicles",
|
||||
branches_used: "Branches",
|
||||
employees_used: "Employees",
|
||||
branches: "branches",
|
||||
employees: "employees",
|
||||
status: "Status",
|
||||
subscription_history: "Subscription History",
|
||||
history_date: "Date",
|
||||
@@ -2844,10 +2969,109 @@ var garages = {
|
||||
no_fleet_access: "You don't have permission to view fleet data."
|
||||
}
|
||||
};
|
||||
var users = {
|
||||
title: "User Management",
|
||||
subtitle: "User list, search, filters and bulk actions",
|
||||
loading: "Loading users...",
|
||||
retry: "Retry",
|
||||
search_placeholder: "Search by email, name or phone...",
|
||||
all_statuses: "All statuses",
|
||||
status_active: "Active",
|
||||
status_inactive: "Inactive",
|
||||
status_deleted: "Deleted",
|
||||
status_banned: "Banned",
|
||||
all_roles: "All roles",
|
||||
role_user: "User",
|
||||
role_admin: "Admin",
|
||||
role_staff: "Staff",
|
||||
role_superadmin: "Superadmin",
|
||||
all_plans: "All plans",
|
||||
plan_free: "Free",
|
||||
plan_premium: "Premium",
|
||||
plan_enterprise: "Enterprise",
|
||||
total_users: "Total Users",
|
||||
active_users: "Active Users",
|
||||
deleted_users: "Deleted Users",
|
||||
banned_users: "Banned Users",
|
||||
new_today: "New Today",
|
||||
filtered_results: "Filtered results",
|
||||
selected_count: "{count} user(s) selected",
|
||||
bulk_activate: "Activate",
|
||||
bulk_deactivate: "Deactivate",
|
||||
clear_selection: "Clear",
|
||||
email: "Email",
|
||||
name: "Name",
|
||||
role: "Role",
|
||||
status: "Status",
|
||||
"package": "Package",
|
||||
registration: "Registration",
|
||||
language: "Language",
|
||||
actions: "Actions",
|
||||
view: "View",
|
||||
activate: "Activate",
|
||||
deactivate: "Deactivate",
|
||||
no_results: "No users match your search.",
|
||||
showing: "Showing",
|
||||
prev: "Previous",
|
||||
next: "Next",
|
||||
details: {
|
||||
loading: "Loading user details...",
|
||||
retry: "Retry",
|
||||
back: "Back to Users",
|
||||
edit: "Edit User",
|
||||
edit_user: "Edit User",
|
||||
profile_tab: "Profile",
|
||||
memberships_tab: "Memberships",
|
||||
account_info: "Account Information",
|
||||
user_id: "User ID",
|
||||
registration_date: "Registration Date",
|
||||
last_login: "Last Login",
|
||||
ui_mode: "UI Mode",
|
||||
region: "Region",
|
||||
currency: "Currency",
|
||||
subscription_info: "Subscription Information",
|
||||
subscription_expires: "Expires At",
|
||||
scope_level: "Scope Level",
|
||||
scope_id: "Scope ID",
|
||||
max_vehicles: "Max Vehicles",
|
||||
max_garages: "Max Garages",
|
||||
personal_info: "Personal Information",
|
||||
last_name: "Last Name",
|
||||
first_name: "First Name",
|
||||
phone: "Phone",
|
||||
birth_place: "Birth Place",
|
||||
birth_date: "Birth Date",
|
||||
mothers_name: "Mother's Name",
|
||||
mothers_first_name: "Mother's First Name",
|
||||
address: "Address",
|
||||
zip: "ZIP Code",
|
||||
city: "City",
|
||||
street: "Street",
|
||||
house_number: "House Number",
|
||||
no_person_record: "No personal information record found for this user.",
|
||||
memberships: "Organization Memberships",
|
||||
memberships_count: "{count} membership(s)",
|
||||
no_memberships: "This user is not a member of any organization.",
|
||||
org_name: "Organization",
|
||||
org_role: "Role",
|
||||
member_status: "Status",
|
||||
joined_at: "Joined",
|
||||
verified: "Verified",
|
||||
verified_yes: "Yes",
|
||||
verified_no: "No",
|
||||
preferred_language: "Preferred Language",
|
||||
is_active: "Active",
|
||||
is_vip: "VIP",
|
||||
cancel: "Cancel",
|
||||
save: "Save Changes",
|
||||
saving: "Saving..."
|
||||
}
|
||||
};
|
||||
const locale_en_46json_0b63539c = {
|
||||
permissions: permissions,
|
||||
packages: packages,
|
||||
garages: garages
|
||||
garages: garages,
|
||||
users: users
|
||||
};
|
||||
|
||||
// @ts-nocheck
|
||||
@@ -3496,16 +3720,16 @@ _wH6JrtIxmaSoA8lCPWFnE9z4lQeXW6H5z3l5aymEQw
|
||||
const assets = {
|
||||
"/index.mjs": {
|
||||
"type": "text/javascript; charset=utf-8",
|
||||
"etag": "\"253ec-9gWita9FIFOcxrWJJImCHqiAiEc\"",
|
||||
"mtime": "2026-06-26T13:42:06.440Z",
|
||||
"size": 152556,
|
||||
"etag": "\"26f47-FxUsDl+Qw3SM1eutSdt3YslLdnY\"",
|
||||
"mtime": "2026-06-29T13:04:36.052Z",
|
||||
"size": 159559,
|
||||
"path": "index.mjs"
|
||||
},
|
||||
"/index.mjs.map": {
|
||||
"type": "application/json",
|
||||
"etag": "\"83251-gdWWzINLTOdOWQypEY8kD82/CME\"",
|
||||
"mtime": "2026-06-26T13:42:06.440Z",
|
||||
"size": 537169,
|
||||
"etag": "\"83331-ZjNHkLjUBSxWFNgfXByOQYuJexY\"",
|
||||
"mtime": "2026-06-29T13:04:36.052Z",
|
||||
"size": 537393,
|
||||
"path": "index.mjs.map"
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/client/_nuxt/BL04W_c6.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/BL04W_c6.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{K as a,B as i,C as o,h as l,L as c}from"./CGTxeHv7.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{N as a,h as i,G as o,s as l,O as c}from"./Dc9IRd06.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
1
frontend_admin/.nuxt/dist/client/_nuxt/BRSeVrJ6.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/BRSeVrJ6.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/client/_nuxt/BZYi31Gu.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/BZYi31Gu.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{S as _,l as u,m as a}from"./Dc9IRd06.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};
|
||||
1
frontend_admin/.nuxt/dist/client/_nuxt/BkOfz63t.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/BkOfz63t.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/client/_nuxt/BkQDb8NA.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/BkQDb8NA.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/client/_nuxt/BopcD-t8.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/BopcD-t8.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/client/_nuxt/C94yYWrE.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/C94yYWrE.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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"./CGTxeHv7.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"./Dc9IRd06.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};
|
||||
1
frontend_admin/.nuxt/dist/client/_nuxt/CYfDGx4h.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/CYfDGx4h.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{_ as h}from"./BPZZgRgP.js";import{p as v,e as y,g as _,h as k,c as r,a as t,f as S,i as s,t as c,j as u,k as V,l as p,v as m,m as x,b as L,w as C,q as f,o as i,d as N}from"./CGTxeHv7.js";const B=v("/sf_logo.png"),E={class:"min-h-screen bg-slate-900 flex items-center justify-center px-4"},R={class:"w-full max-w-md"},j={class:"bg-slate-800 rounded-xl shadow-2xl border border-slate-700 p-8"},A={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"},P=y({__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=h;return i(),r("div",E,[t("div",R,[e[6]||(e[6]=S('<div class="text-center mb-8"><div class="flex items-center justify-center gap-3 mb-4"><img src="'+B+'" 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",j,[s(o).error?(i(),r("div",A,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{P as default};
|
||||
import{_ as y}from"./C94yYWrE.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"./Dc9IRd06.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};
|
||||
1
frontend_admin/.nuxt/dist/client/_nuxt/CmLwdxkM.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/CmLwdxkM.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as s}from"./DlAUqK2U.js";import{o,c as t,P as r}from"./Dc9IRd06.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
@@ -1 +1 @@
|
||||
import{_ as a}from"./BPZZgRgP.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"./CGTxeHv7.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"./C94yYWrE.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"./Dc9IRd06.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};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as s}from"./DlAUqK2U.js";import{o,c as t,M as r}from"./CGTxeHv7.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
6
frontend_admin/.nuxt/dist/client/_nuxt/Dc9IRd06.js
vendored
Normal file
6
frontend_admin/.nuxt/dist/client/_nuxt/Dc9IRd06.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/client/_nuxt/DmyQTZVv.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/DmyQTZVv.js
vendored
Normal file
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
5
frontend_admin/.nuxt/dist/client/_nuxt/b-bIta7J.js
vendored
Normal file
5
frontend_admin/.nuxt/dist/client/_nuxt/b-bIta7J.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/client/_nuxt/bPZppHdV.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/bPZppHdV.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/client/_nuxt/iRX-7sV5.js
vendored
Normal file
1
frontend_admin/.nuxt/dist/client/_nuxt/iRX-7sV5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{a7 as _,q as u,z as a}from"./CGTxeHv7.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 y(e){const r=t.value.find(R=>R.country_code===e);n.value=r||o.value}function m(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:y,getRegion:m}});export{h as u};
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
import { executeAsync } from "/app/node_modules/unctx/dist/index.mjs";
|
||||
import { e as defineNuxtRouteMiddleware, c as useCookie, n as navigateTo, b as useAuthStore } from "../server.mjs";
|
||||
import { e as defineNuxtRouteMiddleware, d as useCookie, n as navigateTo, c as useAuthStore } from "../server.mjs";
|
||||
import "vue";
|
||||
import "/app/node_modules/ofetch/dist/node.mjs";
|
||||
import "#internal/nuxt/paths";
|
||||
@@ -50,4 +50,4 @@ const auth = defineNuxtRouteMiddleware(async (to, from) => {
|
||||
export {
|
||||
auth as default
|
||||
};
|
||||
//# sourceMappingURL=auth-B3jrOAhu.js.map
|
||||
//# sourceMappingURL=auth-Cq1Vc_k9.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"auth-B3jrOAhu.js","sources":["../../../../middleware/auth.ts"],"sourcesContent":["export default defineNuxtRouteMiddleware(async (to, from) => {\n // Skip auth check on the login page itself\n if (to.path === '/login') {\n return\n }\n\n // Check for the access_token cookie\n const tokenCookie = useCookie('access_token')\n if (!tokenCookie.value) {\n return navigateTo('/login')\n }\n\n // ── RBAC: Verify user role from Pinia authStore ──────────────────────\n const authStore = useAuthStore()\n\n // If the user profile hasn't been loaded yet, try to fetch it\n if (!authStore.user) {\n try {\n await authStore.fetchUser()\n } catch {\n // Token is invalid or expired — clear and redirect to login\n authStore.logout()\n return navigateTo('/login')\n }\n }\n\n // After fetch, check if user is still null (fetch failed silently)\n if (!authStore.user) {\n authStore.logout()\n return navigateTo('/login')\n }\n\n // Allowed staff roles that can access the admin panel\n const allowedRoles = ['SUPERADMIN', 'ADMIN', 'MODERATOR', 'SALES_REP', 'SERVICE_MGR']\n const userRole = (authStore.user.role || '').toUpperCase()\n\n if (!allowedRoles.includes(userRole)) {\n // Standard USER or unknown role — kick them out, clear token, redirect\n authStore.logout()\n return navigateTo('/login')\n }\n})\n"],"names":["__executeAsync"],"mappings":";;;;;;;;;;;;;;;;;;AAEE,MAAA,iCAA0B,OAAA,IAAA,SAAA;AAAA,MAAA,QAAA;AACxB,MAAA,GAAA,SAAA,UAAA;AACF;AAAA,EAGA;AACA,sBAAiB,UAAO,cAAA;AACtB,MAAA,CAAA,mBAAkB;AACpB,WAAA,WAAA,QAAA;AAAA,EAGA;AAGA,oBAAe,aAAM;AACnB,MAAA,CAAA,UAAI,MAAA;AACF,QAAA;AACF;AAAA,MAAA,CAAA,QAAQ,SAAA,IAAAA,aAAA,MAAA,UAAA,UAAA,CAAA,GAAA,MAAA,QAAA,UAAA;AAAA;AAAA,IAEN,QAAA;AACA;AACF,aAAA,WAAA,QAAA;AAAA,IACF;AAAA,EAGA;AACE,MAAA,CAAA,UAAU,MAAO;AACjB;AACF,WAAA,WAAA,QAAA;AAAA,EAGA;AACA,QAAM,eAAY,CAAA,cAAe,SAAY,aAAY,aAAA,aAAA;AAEzD,QAAK,YAAa,UAAS,KAAA,QAAW,IAAA,YAAA;AAEpC,MAAA,CAAA,aAAU,SAAO,QAAA,GAAA;AACjB;AACF,WAAA,WAAA,QAAA;AAAA,EACF;;"}
|
||||
{"version":3,"file":"auth-Cq1Vc_k9.js","sources":["../../../../middleware/auth.ts"],"sourcesContent":["export default defineNuxtRouteMiddleware(async (to, from) => {\n // Skip auth check on the login page itself\n if (to.path === '/login') {\n return\n }\n\n // Check for the access_token cookie\n const tokenCookie = useCookie('access_token')\n if (!tokenCookie.value) {\n return navigateTo('/login')\n }\n\n // ── RBAC: Verify user role from Pinia authStore ──────────────────────\n const authStore = useAuthStore()\n\n // If the user profile hasn't been loaded yet, try to fetch it\n if (!authStore.user) {\n try {\n await authStore.fetchUser()\n } catch {\n // Token is invalid or expired — clear and redirect to login\n authStore.logout()\n return navigateTo('/login')\n }\n }\n\n // After fetch, check if user is still null (fetch failed silently)\n if (!authStore.user) {\n authStore.logout()\n return navigateTo('/login')\n }\n\n // Allowed staff roles that can access the admin panel\n const allowedRoles = ['SUPERADMIN', 'ADMIN', 'MODERATOR', 'SALES_REP', 'SERVICE_MGR']\n const userRole = (authStore.user.role || '').toUpperCase()\n\n if (!allowedRoles.includes(userRole)) {\n // Standard USER or unknown role — kick them out, clear token, redirect\n authStore.logout()\n return navigateTo('/login')\n }\n})\n"],"names":["__executeAsync"],"mappings":";;;;;;;;;;;;;;;;;;AAEE,MAAA,iCAA0B,OAAA,IAAA,SAAA;AAAA,MAAA,QAAA;AACxB,MAAA,GAAA,SAAA,UAAA;AACF;AAAA,EAGA;AACA,sBAAiB,UAAO,cAAA;AACtB,MAAA,CAAA,mBAAkB;AACpB,WAAA,WAAA,QAAA;AAAA,EAGA;AAGA,oBAAe,aAAM;AACnB,MAAA,CAAA,UAAI,MAAA;AACF,QAAA;AACF;AAAA,MAAA,CAAA,QAAQ,SAAA,IAAAA,aAAA,MAAA,UAAA,UAAA,CAAA,GAAA,MAAA,QAAA,UAAA;AAAA;AAAA,IAEN,QAAA;AACA;AACF,aAAA,WAAA,QAAA;AAAA,IACF;AAAA,EAGA;AACE,MAAA,CAAA,UAAU,MAAO;AACjB;AACF,WAAA,WAAA,QAAA;AAAA,EAGA;AACA,QAAM,eAAY,CAAA,cAAe,SAAY,aAAY,aAAA,aAAA;AAEzD,QAAK,YAAa,UAAS,KAAA,QAAW,IAAA,YAAA;AAEpC,MAAA,CAAA,aAAU,SAAO,QAAA,GAAA;AACjB;AACF,WAAA,WAAA,QAAA;AAAA,EACF;;"}
|
||||
@@ -1 +1 @@
|
||||
{"file":"auth-B3jrOAhu.js","mappings":";;;;;;;;;;;;;;;;;;AAEE,MAAA,iCAA0B,OAAA,IAAA,SAAA;AAAA,MAAA,QAAA;AACxB,MAAA,GAAA,SAAA,UAAA;AACF;AAAA,EAGA;AACA,sBAAiB,UAAO,cAAA;AACtB,MAAA,CAAA,mBAAkB;AACpB,WAAA,WAAA,QAAA;AAAA,EAGA;AAGA,oBAAe,aAAM;AACnB,MAAA,CAAA,UAAI,MAAA;AACF,QAAA;AACF;AAAA,MAAA,CAAA,QAAQ,SAAA,IAAAA,aAAA,MAAA,UAAA,UAAA,CAAA,GAAA,MAAA,QAAA,UAAA;AAAA;AAAA,IAEN,QAAA;AACA;AACF,aAAA,WAAA,QAAA;AAAA,IACF;AAAA,EAGA;AACE,MAAA,CAAA,UAAU,MAAO;AACjB;AACF,WAAA,WAAA,QAAA;AAAA,EAGA;AACA,QAAM,eAAY,CAAA,cAAe,SAAY,aAAY,aAAA,aAAA;AAEzD,QAAK,YAAa,UAAS,KAAA,QAAW,IAAA,YAAA;AAEpC,MAAA,CAAA,aAAU,SAAO,QAAA,GAAA;AACjB;AACF,WAAA,WAAA,QAAA;AAAA,EACF;;","names":["__executeAsync"],"sources":["../../../../middleware/auth.ts"],"sourcesContent":["export default defineNuxtRouteMiddleware(async (to, from) => {\n // Skip auth check on the login page itself\n if (to.path === '/login') {\n return\n }\n\n // Check for the access_token cookie\n const tokenCookie = useCookie('access_token')\n if (!tokenCookie.value) {\n return navigateTo('/login')\n }\n\n // ── RBAC: Verify user role from Pinia authStore ──────────────────────\n const authStore = useAuthStore()\n\n // If the user profile hasn't been loaded yet, try to fetch it\n if (!authStore.user) {\n try {\n await authStore.fetchUser()\n } catch {\n // Token is invalid or expired — clear and redirect to login\n authStore.logout()\n return navigateTo('/login')\n }\n }\n\n // After fetch, check if user is still null (fetch failed silently)\n if (!authStore.user) {\n authStore.logout()\n return navigateTo('/login')\n }\n\n // Allowed staff roles that can access the admin panel\n const allowedRoles = ['SUPERADMIN', 'ADMIN', 'MODERATOR', 'SALES_REP', 'SERVICE_MGR']\n const userRole = (authStore.user.role || '').toUpperCase()\n\n if (!allowedRoles.includes(userRole)) {\n // Standard USER or unknown role — kick them out, clear token, redirect\n authStore.logout()\n return navigateTo('/login')\n }\n})\n"],"version":3}
|
||||
{"file":"auth-Cq1Vc_k9.js","mappings":";;;;;;;;;;;;;;;;;;AAEE,MAAA,iCAA0B,OAAA,IAAA,SAAA;AAAA,MAAA,QAAA;AACxB,MAAA,GAAA,SAAA,UAAA;AACF;AAAA,EAGA;AACA,sBAAiB,UAAO,cAAA;AACtB,MAAA,CAAA,mBAAkB;AACpB,WAAA,WAAA,QAAA;AAAA,EAGA;AAGA,oBAAe,aAAM;AACnB,MAAA,CAAA,UAAI,MAAA;AACF,QAAA;AACF;AAAA,MAAA,CAAA,QAAQ,SAAA,IAAAA,aAAA,MAAA,UAAA,UAAA,CAAA,GAAA,MAAA,QAAA,UAAA;AAAA;AAAA,IAEN,QAAA;AACA;AACF,aAAA,WAAA,QAAA;AAAA,IACF;AAAA,EAGA;AACE,MAAA,CAAA,UAAU,MAAO;AACjB;AACF,WAAA,WAAA,QAAA;AAAA,EAGA;AACA,QAAM,eAAY,CAAA,cAAe,SAAY,aAAY,aAAA,aAAA;AAEzD,QAAK,YAAa,UAAS,KAAA,QAAW,IAAA,YAAA;AAEpC,MAAA,CAAA,aAAU,SAAO,QAAA,GAAA;AACjB;AACF,WAAA,WAAA,QAAA;AAAA,EACF;;","names":["__executeAsync"],"sources":["../../../../middleware/auth.ts"],"sourcesContent":["export default defineNuxtRouteMiddleware(async (to, from) => {\n // Skip auth check on the login page itself\n if (to.path === '/login') {\n return\n }\n\n // Check for the access_token cookie\n const tokenCookie = useCookie('access_token')\n if (!tokenCookie.value) {\n return navigateTo('/login')\n }\n\n // ── RBAC: Verify user role from Pinia authStore ──────────────────────\n const authStore = useAuthStore()\n\n // If the user profile hasn't been loaded yet, try to fetch it\n if (!authStore.user) {\n try {\n await authStore.fetchUser()\n } catch {\n // Token is invalid or expired — clear and redirect to login\n authStore.logout()\n return navigateTo('/login')\n }\n }\n\n // After fetch, check if user is still null (fetch failed silently)\n if (!authStore.user) {\n authStore.logout()\n return navigateTo('/login')\n }\n\n // Allowed staff roles that can access the admin panel\n const allowedRoles = ['SUPERADMIN', 'ADMIN', 'MODERATOR', 'SALES_REP', 'SERVICE_MGR']\n const userRole = (authStore.user.role || '').toUpperCase()\n\n if (!allowedRoles.includes(userRole)) {\n // Standard USER or unknown role — kick them out, clear token, redirect\n authStore.logout()\n return navigateTo('/login')\n }\n})\n"],"version":3}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { _ as __nuxt_component_0 } from "./nuxt-link-C5PnX__I.js";
|
||||
import { _ as __nuxt_component_0 } from "./nuxt-link-Ci8vU-Yt.js";
|
||||
import { defineComponent, computed, ref, mergeProps, withCtx, createVNode, toDisplayString, unref, openBlock, createBlock, createTextVNode, useSSRContext } from "vue";
|
||||
import { ssrRenderAttrs, ssrRenderClass, ssrRenderComponent, ssrRenderAttr, ssrRenderList, ssrRenderStyle, ssrInterpolate, ssrRenderSlot } from "vue/server-renderer";
|
||||
import { publicAssetsURL } from "#internal/nuxt/paths";
|
||||
import { useRoute } from "vue-router";
|
||||
import { u as useRegionStore } from "./region-0mvXqaMM.js";
|
||||
import { a as useRouter, b as useAuthStore, d as useI18n } from "../server.mjs";
|
||||
import { b as useRouter, c as useAuthStore, a as useI18n } from "../server.mjs";
|
||||
import "/app/node_modules/klona/dist/index.mjs";
|
||||
import "/app/node_modules/hookable/dist/index.mjs";
|
||||
import "/app/node_modules/ufo/dist/index.mjs";
|
||||
@@ -71,10 +71,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
||||
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" /></svg>',
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
/* P0 HOTFIX: placeholder until /users page is built */
|
||||
path: "/users",
|
||||
label: "Felhasználók listája",
|
||||
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" /></svg>'
|
||||
},
|
||||
{
|
||||
path: "/persons",
|
||||
label: "Személyek",
|
||||
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>'
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -117,6 +121,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
||||
if (route.path === "/") return "Dashboard";
|
||||
if (route.path.startsWith("/permissions")) return "Jogosultság Kezelés";
|
||||
if (route.path.startsWith("/garages")) return "Garázsok";
|
||||
if (route.path.startsWith("/persons")) return "Személyek";
|
||||
if (route.path.startsWith("/users")) return "Felhasználók";
|
||||
if (route.path.startsWith("/logs")) return "Rendszernaplók";
|
||||
if (route.path.startsWith("/packages")) return "Csomagok";
|
||||
@@ -287,4 +292,4 @@ _sfc_main.setup = (props, ctx) => {
|
||||
export {
|
||||
_sfc_main as default
|
||||
};
|
||||
//# sourceMappingURL=default-C9mWt9Pc.js.map
|
||||
//# sourceMappingURL=default-C8swInD9.js.map
|
||||
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/server/_nuxt/default-C8swInD9.js.map.json
vendored
Normal file
1
frontend_admin/.nuxt/dist/server/_nuxt/default-C8swInD9.js.map.json
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -58,6 +58,7 @@ const resource = {
|
||||
"allowances": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Allowances" } },
|
||||
"max_vehicles": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Max Vehicles" } },
|
||||
"max_garages": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Max Garages" } },
|
||||
"max_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Max Users" } },
|
||||
"monthly_credits": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Monthly Credits" } },
|
||||
"tier_level": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Level" } },
|
||||
"form_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "System Name" } },
|
||||
@@ -136,6 +137,16 @@ const resource = {
|
||||
"subscription_updated": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "subscription updated successfully!" } },
|
||||
"view_details": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Details" } },
|
||||
"indefinite": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Indefinite" } },
|
||||
"addons_title": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Add-ons" } },
|
||||
"addon_vehicles": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Vehicle" } },
|
||||
"addon_branches": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Branch" } },
|
||||
"addon_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Employee" } },
|
||||
"active_addons": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Active Add-ons" } },
|
||||
"no_active_addons": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "No active add-ons" } },
|
||||
"addon_vehicles_expiration": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Vehicle Expiration" } },
|
||||
"addon_branches_expiration": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Branch Expiration" } },
|
||||
"addon_users_expiration": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Employee Expiration" } },
|
||||
"addon_expiration_hint": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "If empty, existing expiration dates remain." } },
|
||||
"details": {
|
||||
"loading": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Loading garage details..." } },
|
||||
"retry": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Retry" } },
|
||||
@@ -210,6 +221,8 @@ const resource = {
|
||||
"vehicles_used": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Vehicles" } },
|
||||
"branches_used": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Branches" } },
|
||||
"employees_used": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Employees" } },
|
||||
"branches": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "branches" } },
|
||||
"employees": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "employees" } },
|
||||
"status": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Status" } },
|
||||
"subscription_history": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Subscription History" } },
|
||||
"history_date": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Date" } },
|
||||
@@ -292,9 +305,107 @@ const resource = {
|
||||
"no_vehicles": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "No vehicles assigned to this garage yet." } },
|
||||
"no_fleet_access": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "You don't have permission to view fleet data." } }
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"title": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "User Management" } },
|
||||
"subtitle": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "User list, search, filters and bulk actions" } },
|
||||
"loading": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Loading users..." } },
|
||||
"retry": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Retry" } },
|
||||
"search_placeholder": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Search by email, name or phone..." } },
|
||||
"all_statuses": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "All statuses" } },
|
||||
"status_active": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Active" } },
|
||||
"status_inactive": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Inactive" } },
|
||||
"status_deleted": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Deleted" } },
|
||||
"status_banned": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Banned" } },
|
||||
"all_roles": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "All roles" } },
|
||||
"role_user": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "User" } },
|
||||
"role_admin": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Admin" } },
|
||||
"role_staff": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Staff" } },
|
||||
"role_superadmin": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Superadmin" } },
|
||||
"all_plans": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "All plans" } },
|
||||
"plan_free": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Free" } },
|
||||
"plan_premium": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Premium" } },
|
||||
"plan_enterprise": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Enterprise" } },
|
||||
"total_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Total Users" } },
|
||||
"active_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Active Users" } },
|
||||
"deleted_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Deleted Users" } },
|
||||
"banned_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Banned Users" } },
|
||||
"new_today": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "New Today" } },
|
||||
"filtered_results": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Filtered results" } },
|
||||
"selected_count": { "t": 0, "b": { "t": 2, "i": [{ "t": 4, "k": "count" }, { "t": 3, "v": " user(s) selected" }] } },
|
||||
"bulk_activate": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Activate" } },
|
||||
"bulk_deactivate": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Deactivate" } },
|
||||
"clear_selection": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Clear" } },
|
||||
"email": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Email" } },
|
||||
"name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Name" } },
|
||||
"role": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Role" } },
|
||||
"status": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Status" } },
|
||||
"package": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Package" } },
|
||||
"registration": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Registration" } },
|
||||
"language": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Language" } },
|
||||
"actions": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Actions" } },
|
||||
"view": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "View" } },
|
||||
"activate": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Activate" } },
|
||||
"deactivate": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Deactivate" } },
|
||||
"no_results": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "No users match your search." } },
|
||||
"showing": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Showing" } },
|
||||
"prev": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Previous" } },
|
||||
"next": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Next" } },
|
||||
"details": {
|
||||
"loading": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Loading user details..." } },
|
||||
"retry": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Retry" } },
|
||||
"back": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Back to Users" } },
|
||||
"edit": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Edit User" } },
|
||||
"edit_user": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Edit User" } },
|
||||
"profile_tab": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Profile" } },
|
||||
"memberships_tab": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Memberships" } },
|
||||
"account_info": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Account Information" } },
|
||||
"user_id": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "User ID" } },
|
||||
"registration_date": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Registration Date" } },
|
||||
"last_login": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Last Login" } },
|
||||
"ui_mode": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "UI Mode" } },
|
||||
"region": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Region" } },
|
||||
"currency": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Currency" } },
|
||||
"subscription_info": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Subscription Information" } },
|
||||
"subscription_expires": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Expires At" } },
|
||||
"scope_level": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Scope Level" } },
|
||||
"scope_id": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Scope ID" } },
|
||||
"max_vehicles": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Max Vehicles" } },
|
||||
"max_garages": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Max Garages" } },
|
||||
"personal_info": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Personal Information" } },
|
||||
"last_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Last Name" } },
|
||||
"first_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "First Name" } },
|
||||
"phone": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Phone" } },
|
||||
"birth_place": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Birth Place" } },
|
||||
"birth_date": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Birth Date" } },
|
||||
"mothers_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Mother's Name" } },
|
||||
"mothers_first_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Mother's First Name" } },
|
||||
"address": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Address" } },
|
||||
"zip": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "ZIP Code" } },
|
||||
"city": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "City" } },
|
||||
"street": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Street" } },
|
||||
"house_number": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "House Number" } },
|
||||
"no_person_record": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "No personal information record found for this user." } },
|
||||
"memberships": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Organization Memberships" } },
|
||||
"memberships_count": { "t": 0, "b": { "t": 2, "i": [{ "t": 4, "k": "count" }, { "t": 3, "v": " membership(s)" }] } },
|
||||
"no_memberships": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "This user is not a member of any organization." } },
|
||||
"org_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Organization" } },
|
||||
"org_role": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Role" } },
|
||||
"member_status": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Status" } },
|
||||
"joined_at": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Joined" } },
|
||||
"verified": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Verified" } },
|
||||
"verified_yes": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Yes" } },
|
||||
"verified_no": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "No" } },
|
||||
"preferred_language": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Preferred Language" } },
|
||||
"is_active": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Active" } },
|
||||
"is_vip": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "VIP" } },
|
||||
"cancel": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Cancel" } },
|
||||
"save": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Save Changes" } },
|
||||
"saving": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Saving..." } }
|
||||
}
|
||||
}
|
||||
};
|
||||
export {
|
||||
resource as default
|
||||
};
|
||||
//# sourceMappingURL=en-BI6r-iRW.js.map
|
||||
//# sourceMappingURL=en-BPBMPZpX.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"en-BI6r-iRW.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
||||
{"version":3,"file":"en-BPBMPZpX.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
||||
@@ -1 +1 @@
|
||||
{"file":"en-BI6r-iRW.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","names":[],"sources":[],"sourcesContent":[],"version":3}
|
||||
{"file":"en-BPBMPZpX.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","names":[],"sources":[],"sourcesContent":[],"version":3}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"entry-styles-1.mjs-Be0Mk_0H.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
||||
@@ -1 +0,0 @@
|
||||
{"file":"entry-styles-1.mjs-Be0Mk_0H.js","mappings":";","names":[],"sources":[],"sourcesContent":[],"version":3}
|
||||
5
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-1.mjs-C-JbU1OR.js
vendored
Normal file
5
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-1.mjs-C-JbU1OR.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-1.mjs-C-JbU1OR.js.map
vendored
Normal file
1
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-1.mjs-C-JbU1OR.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"entry-styles-1.mjs-C-JbU1OR.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
||||
1
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-1.mjs-C-JbU1OR.js.map.json
vendored
Normal file
1
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-1.mjs-C-JbU1OR.js.map.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"file":"entry-styles-1.mjs-C-JbU1OR.js","mappings":";","names":[],"sources":[],"sourcesContent":[],"version":3}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"entry-styles-2.mjs-CiZbT56B.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
||||
@@ -1 +0,0 @@
|
||||
{"file":"entry-styles-2.mjs-CiZbT56B.js","mappings":";","names":[],"sources":[],"sourcesContent":[],"version":3}
|
||||
5
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-2.mjs-e4rjr5TT.js
vendored
Normal file
5
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-2.mjs-e4rjr5TT.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-2.mjs-e4rjr5TT.js.map
vendored
Normal file
1
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-2.mjs-e4rjr5TT.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"entry-styles-2.mjs-e4rjr5TT.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
||||
1
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-2.mjs-e4rjr5TT.js.map.json
vendored
Normal file
1
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles-2.mjs-e4rjr5TT.js.map.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"file":"entry-styles-2.mjs-e4rjr5TT.js","mappings":";","names":[],"sources":[],"sourcesContent":[],"version":3}
|
||||
8
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles.DI3TVdQz.mjs
vendored
Normal file
8
frontend_admin/.nuxt/dist/server/_nuxt/entry-styles.DI3TVdQz.mjs
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import style_0 from "./entry-styles-1.mjs-C-JbU1OR.js";
|
||||
import style_1 from "./entry-styles-2.mjs-e4rjr5TT.js";
|
||||
import style_2 from "./entry-styles-3.mjs-C1rWf53M.js";
|
||||
export default [
|
||||
style_0,
|
||||
style_1,
|
||||
style_2
|
||||
]
|
||||
@@ -1,8 +0,0 @@
|
||||
import style_0 from "./entry-styles-1.mjs-Be0Mk_0H.js";
|
||||
import style_1 from "./entry-styles-2.mjs-CiZbT56B.js";
|
||||
import style_2 from "./entry-styles-3.mjs-C1rWf53M.js";
|
||||
export default [
|
||||
style_0,
|
||||
style_1,
|
||||
style_2
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
import { _ as __nuxt_component_0 } from "./nuxt-link-C5PnX__I.js";
|
||||
import { _ as __nuxt_component_0 } from "./nuxt-link-Ci8vU-Yt.js";
|
||||
import { mergeProps, withCtx, createTextVNode, toDisplayString, useSSRContext } from "vue";
|
||||
import { ssrRenderAttrs, ssrInterpolate, ssrRenderComponent } from "vue/server-renderer";
|
||||
import { _ as _export_sfc } from "./_plugin-vue_export-helper-1tPrXgE0.js";
|
||||
@@ -94,4 +94,4 @@ const error404 = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-1
|
||||
export {
|
||||
error404 as default
|
||||
};
|
||||
//# sourceMappingURL=error-404-DPt4byml.js.map
|
||||
//# sourceMappingURL=error-404-qU7XehyZ.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -58,6 +58,7 @@ const resource = {
|
||||
"allowances": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Korlátok" } },
|
||||
"max_vehicles": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Max Jármű" } },
|
||||
"max_garages": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Max Garázs" } },
|
||||
"max_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Max Dolgozó" } },
|
||||
"monthly_credits": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Havi Kredit" } },
|
||||
"tier_level": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Szint" } },
|
||||
"form_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Rendszer Név" } },
|
||||
@@ -136,6 +137,16 @@ const resource = {
|
||||
"subscription_updated": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "előfizetése sikeresen frissítve!" } },
|
||||
"view_details": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Részletek" } },
|
||||
"indefinite": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Határozatlan" } },
|
||||
"addons_title": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Kiegészítők (Add-ons)" } },
|
||||
"addon_vehicles": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Jármű" } },
|
||||
"addon_branches": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Telephely" } },
|
||||
"addon_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Dolgozó" } },
|
||||
"active_addons": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Aktív kiegészítők" } },
|
||||
"no_active_addons": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Nincsenek aktív kiegészítők" } },
|
||||
"addon_vehicles_expiration": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Jármű lejárati dátuma" } },
|
||||
"addon_branches_expiration": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Telephely lejárati dátuma" } },
|
||||
"addon_users_expiration": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Extra Dolgozó lejárati dátuma" } },
|
||||
"addon_expiration_hint": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Ha üres, a meglévő lejárati dátumok megmaradnak." } },
|
||||
"details": {
|
||||
"loading": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Garázs adatainak betöltése..." } },
|
||||
"retry": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Újra" } },
|
||||
@@ -210,6 +221,8 @@ const resource = {
|
||||
"vehicles_used": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Járművek" } },
|
||||
"branches_used": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Telephelyek" } },
|
||||
"employees_used": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Dolgozók" } },
|
||||
"branches": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "telephely" } },
|
||||
"employees": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "dolgozó" } },
|
||||
"status": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Státusz" } },
|
||||
"subscription_history": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Előfizetési Előzmények" } },
|
||||
"history_date": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Dátum" } },
|
||||
@@ -292,9 +305,107 @@ const resource = {
|
||||
"no_vehicles": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Ehhez a garázshoz még nincsenek járművek rendelve." } },
|
||||
"no_fleet_access": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Nincs jogosultságod a flotta adatok megtekintéséhez." } }
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"title": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Felhasználó Kezelés" } },
|
||||
"subtitle": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Felhasználók listája, keresés, szűrés és tömeges műveletek" } },
|
||||
"loading": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Felhasználók betöltése..." } },
|
||||
"retry": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Újra" } },
|
||||
"search_placeholder": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Keresés email, név vagy telefonszám alapján..." } },
|
||||
"all_statuses": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Összes státusz" } },
|
||||
"status_active": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Aktív" } },
|
||||
"status_inactive": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Inaktív" } },
|
||||
"status_deleted": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Törölt" } },
|
||||
"status_banned": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Kitiltott" } },
|
||||
"all_roles": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Összes szerepkör" } },
|
||||
"role_user": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Felhasználó" } },
|
||||
"role_admin": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Admin" } },
|
||||
"role_staff": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Munkatárs" } },
|
||||
"role_superadmin": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Superadmin" } },
|
||||
"all_plans": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Összes csomag" } },
|
||||
"plan_free": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Ingyenes" } },
|
||||
"plan_premium": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Prémium" } },
|
||||
"plan_enterprise": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Vállalati" } },
|
||||
"total_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Összes Felhasználó" } },
|
||||
"active_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Aktív Felhasználók" } },
|
||||
"deleted_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Törölt Felhasználók" } },
|
||||
"banned_users": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Kitiltott Felhasználók" } },
|
||||
"new_today": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Ma Regisztrált" } },
|
||||
"filtered_results": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Szűrt találat" } },
|
||||
"selected_count": { "t": 0, "b": { "t": 2, "i": [{ "t": 4, "k": "count" }, { "t": 3, "v": " felhasználó kiválasztva" }] } },
|
||||
"bulk_activate": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Aktiválás" } },
|
||||
"bulk_deactivate": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Deaktiválás" } },
|
||||
"clear_selection": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Kijelölés törlése" } },
|
||||
"email": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "E-mail" } },
|
||||
"name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Név" } },
|
||||
"role": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Szerepkör" } },
|
||||
"status": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Státusz" } },
|
||||
"package": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Csomag" } },
|
||||
"registration": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Regisztráció" } },
|
||||
"language": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Nyelv" } },
|
||||
"actions": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Műveletek" } },
|
||||
"view": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Megtekintés" } },
|
||||
"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" } },
|
||||
"no_results": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Nincs a keresésnek megfelelő felhasználó." } },
|
||||
"showing": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Mutató" } },
|
||||
"prev": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Előző" } },
|
||||
"next": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Következő" } },
|
||||
"details": {
|
||||
"loading": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Felhasználó adatainak betöltése..." } },
|
||||
"retry": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Újra" } },
|
||||
"back": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Vissza a felhasználókhoz" } },
|
||||
"edit": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Felhasználó szerkesztése" } },
|
||||
"edit_user": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Felhasználó szerkesztése" } },
|
||||
"profile_tab": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Profil" } },
|
||||
"memberships_tab": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Tagságok" } },
|
||||
"account_info": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Fiók Információk" } },
|
||||
"user_id": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Felhasználó ID" } },
|
||||
"registration_date": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Regisztráció Dátuma" } },
|
||||
"last_login": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Utolsó Belépés" } },
|
||||
"ui_mode": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "UI Mód" } },
|
||||
"region": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Régió" } },
|
||||
"currency": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Pénznem" } },
|
||||
"subscription_info": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Előfizetés Információk" } },
|
||||
"subscription_expires": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Lejárat Dátuma" } },
|
||||
"scope_level": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Hatáskör Szint" } },
|
||||
"scope_id": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Hatáskör ID" } },
|
||||
"max_vehicles": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Max Járművek" } },
|
||||
"max_garages": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Max Garázsok" } },
|
||||
"personal_info": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Személyes Információk" } },
|
||||
"last_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Vezetéknév" } },
|
||||
"first_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Keresztnév" } },
|
||||
"phone": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Telefonszám" } },
|
||||
"birth_place": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Születési Hely" } },
|
||||
"birth_date": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Születési Dátum" } },
|
||||
"mothers_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Anyja Vezetékneve" } },
|
||||
"mothers_first_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Anyja Keresztneve" } },
|
||||
"address": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Cím" } },
|
||||
"zip": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Irányítószám" } },
|
||||
"city": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Város" } },
|
||||
"street": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Utca" } },
|
||||
"house_number": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Házszám" } },
|
||||
"no_person_record": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Nem található személyes adat ehhez a felhasználóhoz." } },
|
||||
"memberships": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Szervezeti Tagságok" } },
|
||||
"memberships_count": { "t": 0, "b": { "t": 2, "i": [{ "t": 4, "k": "count" }, { "t": 3, "v": " tagság" }] } },
|
||||
"no_memberships": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Ez a felhasználó nem tagja egyetlen szervezetnek sem." } },
|
||||
"org_name": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Szervezet" } },
|
||||
"org_role": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Szerepkör" } },
|
||||
"member_status": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Státusz" } },
|
||||
"joined_at": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Csatlakozott" } },
|
||||
"verified": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Ellenőrzött" } },
|
||||
"verified_yes": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Igen" } },
|
||||
"verified_no": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Nem" } },
|
||||
"preferred_language": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Előnyben Részesített Nyelv" } },
|
||||
"is_active": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Aktív" } },
|
||||
"is_vip": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "VIP" } },
|
||||
"cancel": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Mégse" } },
|
||||
"save": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Változtatások Mentése" } },
|
||||
"saving": { "t": 0, "b": { "t": 2, "i": [{ "t": 3 }], "s": "Mentés..." } }
|
||||
}
|
||||
}
|
||||
};
|
||||
export {
|
||||
resource as default
|
||||
};
|
||||
//# sourceMappingURL=hu-Ck1VzT5S.js.map
|
||||
//# sourceMappingURL=hu-DFYycw88.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"hu-Ck1VzT5S.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
||||
{"version":3,"file":"hu-DFYycw88.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
||||
@@ -1 +1 @@
|
||||
{"file":"hu-Ck1VzT5S.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","names":[],"sources":[],"sourcesContent":[],"version":3}
|
||||
{"file":"hu-DFYycw88.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","names":[],"sources":[],"sourcesContent":[],"version":3}
|
||||
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
@@ -2,7 +2,7 @@ import { defineComponent, ref, computed, unref, useSSRContext } from "vue";
|
||||
import { ssrRenderAttrs, ssrInterpolate, ssrRenderClass, ssrRenderList, ssrIncludeBooleanAttr, ssrRenderAttr } from "vue/server-renderer";
|
||||
import "/app/node_modules/hookable/dist/index.mjs";
|
||||
import "/app/node_modules/klona/dist/index.mjs";
|
||||
import { d as useI18n } from "../server.mjs";
|
||||
import { a as useI18n } from "../server.mjs";
|
||||
import "/app/node_modules/ofetch/dist/node.mjs";
|
||||
import "#internal/nuxt/paths";
|
||||
import "/app/node_modules/unctx/dist/index.mjs";
|
||||
@@ -209,4 +209,4 @@ _sfc_main.setup = (props, ctx) => {
|
||||
export {
|
||||
_sfc_main as default
|
||||
};
|
||||
//# sourceMappingURL=index-DgwDY6Ie.js.map
|
||||
//# sourceMappingURL=index-BD2eKDSC.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
481
frontend_admin/.nuxt/dist/server/_nuxt/index-BXFH9zj5.js
vendored
Normal file
481
frontend_admin/.nuxt/dist/server/_nuxt/index-BXFH9zj5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/server/_nuxt/index-BXFH9zj5.js.map
vendored
Normal file
1
frontend_admin/.nuxt/dist/server/_nuxt/index-BXFH9zj5.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/server/_nuxt/index-BXFH9zj5.js.map.json
vendored
Normal file
1
frontend_admin/.nuxt/dist/server/_nuxt/index-BXFH9zj5.js.map.json
vendored
Normal file
File diff suppressed because one or more lines are too long
323
frontend_admin/.nuxt/dist/server/_nuxt/index-Bd_e7zBi.js
vendored
Normal file
323
frontend_admin/.nuxt/dist/server/_nuxt/index-Bd_e7zBi.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/server/_nuxt/index-Bd_e7zBi.js.map
vendored
Normal file
1
frontend_admin/.nuxt/dist/server/_nuxt/index-Bd_e7zBi.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/server/_nuxt/index-Bd_e7zBi.js.map.json
vendored
Normal file
1
frontend_admin/.nuxt/dist/server/_nuxt/index-Bd_e7zBi.js.map.json
vendored
Normal file
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
1011
frontend_admin/.nuxt/dist/server/_nuxt/index-CKl-ge5n.js
vendored
Normal file
1011
frontend_admin/.nuxt/dist/server/_nuxt/index-CKl-ge5n.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/server/_nuxt/index-CKl-ge5n.js.map
vendored
Normal file
1
frontend_admin/.nuxt/dist/server/_nuxt/index-CKl-ge5n.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend_admin/.nuxt/dist/server/_nuxt/index-CKl-ge5n.js.map.json
vendored
Normal file
1
frontend_admin/.nuxt/dist/server/_nuxt/index-CKl-ge5n.js.map.json
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
import { _ as __nuxt_component_0 } from "./nuxt-link-C5PnX__I.js";
|
||||
import { _ as __nuxt_component_0 } from "./nuxt-link-Ci8vU-Yt.js";
|
||||
import { defineComponent, ref, watch, computed, withCtx, createTextVNode, toDisplayString, useSSRContext } from "vue";
|
||||
import { ssrRenderAttrs, ssrInterpolate, ssrRenderAttr, ssrIncludeBooleanAttr, ssrLooseContain, ssrLooseEqual, ssrRenderList, ssrRenderClass, ssrRenderComponent } from "vue/server-renderer";
|
||||
import "/app/node_modules/hookable/dist/index.mjs";
|
||||
import { c as useCookie } from "../server.mjs";
|
||||
import { d as useCookie } from "../server.mjs";
|
||||
import "/app/node_modules/ufo/dist/index.mjs";
|
||||
import "/app/node_modules/defu/dist/defu.mjs";
|
||||
import "/app/node_modules/ofetch/dist/node.mjs";
|
||||
@@ -263,4 +263,4 @@ _sfc_main.setup = (props, ctx) => {
|
||||
export {
|
||||
_sfc_main as default
|
||||
};
|
||||
//# sourceMappingURL=index-CzKBE-M2.js.map
|
||||
//# sourceMappingURL=index-CemkpUgu.js.map
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user