felhasználói felületre pü
This commit is contained in:
251
plans/admin_crash_and_user_repair_blueprint.md
Normal file
251
plans/admin_crash_and_user_repair_blueprint.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# 🔧 Admin OOM Crash Fix & User Relations Repair — Code Mode Hand-off
|
||||
|
||||
**Author:** Architect Mode
|
||||
**Date:** 2026-07-26
|
||||
**Target Mode:** `code` (Fast Coder)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Diagnosztikai Összefoglaló
|
||||
|
||||
### Issue 1: Admin Frontend OOM (`admin.servicefinder.hu` → 500)
|
||||
|
||||
**Jelenlegi állapot:**
|
||||
- A [`sf_admin_frontend`](docker-compose.yml:281) konténer jelenleg FUT (742MiB RAM, 0.42%), az OOM korábban történt.
|
||||
- A [`Dockerfile.dev`](frontend_admin/Dockerfile.dev:1) `node:20-slim`-et használ, a CMD `npm run dev` (Nuxt dev mode SSR + HMR).
|
||||
- A [`docker-compose.yml`](docker-compose.yml:289) szekcióban **nincs** `NODE_OPTIONS` környezeti változó és **nincs** `deploy.resources.limits.memory` beállítva.
|
||||
- A bot-scanner forgalom (`.env` probing) csak Vue Router warningokat generál, nem ez okozza az OOM-t.
|
||||
|
||||
**Root Cause:** A Nuxt 3 dev módban az SSR (Server-Side Rendering) + HMR (Hot Module Replacement) + a dashboard oldal API hívása (`/api/v1/admin/users/stats`) együtt képes kimeríteni a Node.js default heap limitjét (~2GB). A `node:20-slim` image-nek nincs explicit `--max-old-space-size` beállítása.
|
||||
|
||||
### Issue 2: User 86 Wallet Status & Missing User Relations
|
||||
|
||||
**User 86 (`zs.gyongyossy@gmail.com`) státusza:**
|
||||
- Wallet: **RENDBEN** — `identity.wallets` id=12, earned=0, purchased=0, service_coins=0, currency=HUF, org_id=45
|
||||
- Subscription: **RENDBEN** — `finance.user_subscriptions` id=9, tier_id=13 (`private_free_v1`), is_active=true
|
||||
|
||||
**6 aktív user-nek NINCS walletje és subscription-je:**
|
||||
|
||||
| ID | Email | Wallet | Subscription |
|
||||
|----|-------|--------|-------------|
|
||||
| **1** | `superadmin@profibot.hu` | ❌ MISSING | ❌ MISSING |
|
||||
| 136 | `no_perm_1782770270_8c9970@test.com` | ❌ MISSING | ❌ MISSING |
|
||||
| 138 | `no_perm_1782778366_243628@test.com` | ❌ MISSING | ❌ MISSING |
|
||||
| 153 | `commission_test_gen2@test.com` | ❌ MISSING | ❌ MISSING |
|
||||
| 154 | `commission_test_gen1@test.com` | ❌ MISSING | ❌ MISSING |
|
||||
| 155 | `commission_test_buyer@test.com` | ❌ MISSING | ❌ MISSING |
|
||||
|
||||
**Végpont viselkedés:** [`get_wallet_summary`](backend/app/services/billing_engine.py:598) `ValueError`-t dob ha nincs wallet → a végpont 404-et ad. Ez helyes.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Javítási Terv
|
||||
|
||||
### FIX-1: Admin Frontend Memory Limit
|
||||
|
||||
**Fájl:** [`docker-compose.yml`](docker-compose.yml:281-299)
|
||||
|
||||
**Módosítás — a `sf_admin_frontend` szekcióban:**
|
||||
|
||||
1. Az `environment` blokkhoz add hozzá:
|
||||
```yaml
|
||||
- NODE_OPTIONS=--max-old-space-size=4096
|
||||
```
|
||||
|
||||
2. Adj hozzá egy `deploy` blokkot:
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 8G
|
||||
```
|
||||
|
||||
**A módosított szekció:**
|
||||
```yaml
|
||||
sf_admin_frontend:
|
||||
build:
|
||||
context: ./frontend_admin
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: sf_admin_frontend
|
||||
env_file: .env
|
||||
ports:
|
||||
- "8502:8502"
|
||||
environment:
|
||||
- NUXT_PORT=8502
|
||||
- NUXT_HOST=0.0.0.0
|
||||
- NUXT_PUBLIC_API_BASE_URL=https://dev.servicefinder.hu
|
||||
- NODE_OPTIONS=--max-old-space-size=4096 # ← NEW
|
||||
volumes:
|
||||
- ./frontend_admin:/app
|
||||
- /app/node_modules
|
||||
networks:
|
||||
- sf_net
|
||||
- shared_db_net
|
||||
restart: unless-stopped
|
||||
deploy: # ← NEW
|
||||
resources:
|
||||
limits:
|
||||
memory: 8G
|
||||
```
|
||||
|
||||
**Futtatás:**
|
||||
```bash
|
||||
docker compose up -d --force-recreate sf_admin_frontend
|
||||
docker compose logs --tail=20 sf_admin_frontend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### FIX-2: User Relations Repair Script
|
||||
|
||||
**Fájl létrehozása:** [`backend/scripts/repair_user_relations.py`](backend/scripts/repair_user_relations.py)
|
||||
|
||||
**Cél:** Minden `is_active = true` user számára biztosítja:
|
||||
1. Personal Organization (`fleet.organizations`) — ha még nincs
|
||||
2. Wallet (`identity.wallets`) — ha még nincs
|
||||
3. User Subscription (`finance.user_subscriptions`, tier=private_free_v1) — ha még nincs
|
||||
|
||||
**Adatbázis séma referencia:**
|
||||
|
||||
| Tábla | Séma | Kulcs mezők |
|
||||
|-------|------|-------------|
|
||||
| `wallets` | `identity` | id (serial PK), user_id (FK), earned_credits, purchased_credits, service_coins, currency, organization_id |
|
||||
| `user_subscriptions` | `finance` | id (serial PK), user_id (FK), tier_id (FK→system.subscription_tiers), valid_from, is_active |
|
||||
| `organizations` | `fleet` | id (serial PK), name, full_name, display_name, folder_slug, owner_id (FK), org_type, status, is_active, ... |
|
||||
|
||||
**Alapértelmezett értékek:**
|
||||
- **Wallet:** `earned_credits=0`, `purchased_credits=0`, `service_coins=0`, `currency='HUF'`
|
||||
- **Subscription:** `tier_id=13` (`private_free_v1`), `valid_from=now()`, `is_active=true`
|
||||
- **Organization:** `name="{user.email}'s Garage"`, `org_type='individual'`, `status='active'`, `owner_id=user.id`
|
||||
|
||||
**Script pszeudokód:**
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
User Relations Repair Script — Idempotens: csak hiányzó rekordokat hoz létre.
|
||||
Futtatás: docker compose exec sf_api python3 /app/backend/scripts/repair_user_relations.py
|
||||
"""
|
||||
import asyncio, sys
|
||||
sys.path.insert(0, '/app')
|
||||
sys.path.insert(0, '/app/backend')
|
||||
|
||||
from app.core.database import async_session_factory
|
||||
from sqlalchemy import select
|
||||
from app.models.identity import User, Wallet
|
||||
from app.models.finance import UserSubscription
|
||||
from app.models.fleet import Organization
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
DEFAULT_TIER_ID = 13
|
||||
DEFAULT_CURRENCY = "HUF"
|
||||
|
||||
async def ensure_organization(db, user):
|
||||
result = await db.execute(select(Organization).where(Organization.owner_id == user.id))
|
||||
org = result.scalar_one_or_none()
|
||||
if org:
|
||||
return org.id
|
||||
folder_slug = f"user-{user.id}-{int(datetime.now(timezone.utc).timestamp())}"
|
||||
org = Organization(
|
||||
name=f"{user.email}'s Garage", full_name=f"{user.email}'s Personal Garage",
|
||||
display_name=f"{user.email}'s Garage", folder_slug=folder_slug,
|
||||
owner_id=user.id, org_type="individual", status="active",
|
||||
is_active=True, is_deleted=False, subscription_plan="FREE",
|
||||
base_asset_limit=5, purchased_extra_slots=0,
|
||||
notification_settings={}, external_integration_config={},
|
||||
is_verified=False, first_registered_at=datetime.now(timezone.utc),
|
||||
current_lifecycle_started_at=datetime.now(timezone.utc),
|
||||
lifecycle_index=1, is_anonymized=False, default_currency="HUF",
|
||||
country_code="HU", language="hu", is_ownership_transferable=False,
|
||||
visual_settings={}, is_affiliate_partner=False, aliases=[],
|
||||
tags=[], settings={}, custom_features={},
|
||||
)
|
||||
db.add(org)
|
||||
await db.flush()
|
||||
return org.id
|
||||
|
||||
async def ensure_wallet(db, user, org_id):
|
||||
result = await db.execute(select(Wallet).where(Wallet.user_id == user.id))
|
||||
if result.scalar_one_or_none():
|
||||
return False
|
||||
wallet = Wallet(user_id=user.id, earned_credits=Decimal('0'),
|
||||
purchased_credits=Decimal('0'), service_coins=Decimal('0'),
|
||||
currency=DEFAULT_CURRENCY, organization_id=org_id)
|
||||
db.add(wallet)
|
||||
await db.flush()
|
||||
return True
|
||||
|
||||
async def ensure_subscription(db, user):
|
||||
result = await db.execute(select(UserSubscription).where(UserSubscription.user_id == user.id))
|
||||
if result.scalar_one_or_none():
|
||||
return False
|
||||
sub = UserSubscription(user_id=user.id, tier_id=DEFAULT_TIER_ID,
|
||||
valid_from=datetime.now(timezone.utc), is_active=True)
|
||||
db.add(sub)
|
||||
await db.flush()
|
||||
return True
|
||||
|
||||
async def repair_all_users():
|
||||
async with async_session_factory() as db:
|
||||
result = await db.execute(select(User).where(User.is_active == True))
|
||||
users = result.scalars().all()
|
||||
stats = {"total": len(users), "wallets_created": 0, "subs_created": 0, "errors": []}
|
||||
for user in users:
|
||||
try:
|
||||
org_id = await ensure_organization(db, user)
|
||||
if await ensure_wallet(db, user, org_id):
|
||||
stats["wallets_created"] += 1
|
||||
if await ensure_subscription(db, user):
|
||||
stats["subs_created"] += 1
|
||||
except Exception as e:
|
||||
stats["errors"].append(f"User {user.id}: {e}")
|
||||
await db.commit()
|
||||
print(f"=== REPAIR: total={stats['total']} wallets_new={stats['wallets_created']} subs_new={stats['subs_created']} errors={len(stats['errors'])}")
|
||||
for e in stats["errors"]: print(f" ERR: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(repair_all_users())
|
||||
```
|
||||
|
||||
**Futtatás:**
|
||||
```bash
|
||||
docker compose exec sf_api python3 /app/backend/scripts/repair_user_relations.py
|
||||
```
|
||||
|
||||
**Ellenőrzés:**
|
||||
```sql
|
||||
SELECT u.id, u.email, w.id as wallet_id, s.id as sub_id
|
||||
FROM identity.users u
|
||||
LEFT JOIN identity.wallets w ON u.id = w.user_id
|
||||
LEFT JOIN finance.user_subscriptions s ON u.id = s.user_id
|
||||
WHERE u.id IN (1, 136, 138, 153, 154, 155);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### FIX-3 (OPTIONAL): get_wallet_summary Auto-Create
|
||||
|
||||
**Architect döntés:** **NEM módosítjuk.** A 404 helyes válasz. A repair script után a probléma megszűnik. A wallet auto-create a regisztrációs flow-ban garantálandó — külön feladat.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Végrehajtási Sorrend (Code Mode)
|
||||
|
||||
| # | Feladat | Fájl | Parancs |
|
||||
|---|---------|------|---------|
|
||||
| 1 | `NODE_OPTIONS` + memory limit | [`docker-compose.yml`](docker-compose.yml:289) | Szerkesztés |
|
||||
| 2 | Admin restart | — | `docker compose up -d --force-recreate sf_admin_frontend` |
|
||||
| 3 | Repair script létrehozása | [`backend/scripts/repair_user_relations.py`](backend/scripts/repair_user_relations.py) | Fájl létrehozása |
|
||||
| 4 | Repair script futtatása | — | `docker compose exec sf_api python3 /app/backend/scripts/repair_user_relations.py` |
|
||||
| 5 | SQL ellenőrzés | — | MCP Postgres query |
|
||||
| 6 | Böngésző ellenőrzés | — | `https://admin.servicefinder.hu/` |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Kockázatok
|
||||
|
||||
1. **superadmin (ID=1):** Lehet hogy szándékosan nincs walletje. Ha nem kívánatos, skip-elni kell ID=1-et a scriptben.
|
||||
2. **Organization folder_slug:** Egyedi kell legyen. A `user-{id}-{timestamp}` minta garantálja.
|
||||
3. **Tier ID=13:** Ellenőrizve — létezik `system.subscription_tiers`-ben, `tier_level=0`.
|
||||
4. **Docker restart:** ~30-60 másodperc downtime a Nuxt újraépítése miatt.
|
||||
5. **A repair script IDEMPOTENS:** Többször futtatható, nem duplikál.
|
||||
200
plans/bugfix_quad_blueprint_2026-07-26.md
Normal file
200
plans/bugfix_quad_blueprint_2026-07-26.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# 🔧 Code Mode Hand-off Blueprint: 4-Bug Fix Bundle
|
||||
|
||||
**Created:** 2026-07-26 | **Mode:** Architect → Code
|
||||
**Target Branch:** `main`
|
||||
|
||||
---
|
||||
|
||||
## 📊 Root Cause Analysis Summary
|
||||
|
||||
| # | Bug | Root Cause File | Severity |
|
||||
|---|-----|----------------|----------|
|
||||
| 1 | Logo navigation traps user in org scope | HeaderLogo.vue:33 | Medium |
|
||||
| 2 | GET /wallet/balance → 500 for user 106 | billing_engine.py:625-633 | Critical |
|
||||
| 3 | "705,000" shown for all users (N/A — no mock found) | N/A — see analysis below | Info |
|
||||
| 4 | [intlify] Not found 'subscription.vehicles' | SubscriptionStatusWidget.vue:67,81 | Low |
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 1: Logo Navigation — Always Redirect to /dashboard
|
||||
|
||||
### File
|
||||
`frontend_app/src/components/header/HeaderLogo.vue`
|
||||
|
||||
### Current Behavior (Lines 32-38)
|
||||
```typescript
|
||||
const targetRoute = computed(() => {
|
||||
if (route.path.startsWith('/organization/')) {
|
||||
const orgId = route.params.id
|
||||
return orgId ? `/organization/${orgId}` : route.path
|
||||
}
|
||||
return '/dashboard'
|
||||
})
|
||||
```
|
||||
|
||||
When user is on `/organization/5/vehicles`, `route.path.startsWith('/organization/')` is `true`, so the logo navigates to `/organization/5` — the org root — NOT to `/dashboard`. This traps the user; there is no way to return to the private garage dashboard via the logo.
|
||||
|
||||
### Root Cause
|
||||
The logo's `targetRoute` logic intentionally scopes the user to the org when on org pages, but this prevents escaping back to `/dashboard`.
|
||||
|
||||
### Fix
|
||||
**Replace the entire `targetRoute` computed with:**
|
||||
```typescript
|
||||
const targetRoute = computed(() => '/dashboard')
|
||||
```
|
||||
|
||||
**Rationale:** The `HeaderCompanySwitcher` component already provides organization context switching. The logo should always be a "global home" escape hatch. This follows the web standard: clicking the brand logo always returns to the root dashboard.
|
||||
|
||||
### Lines to Change
|
||||
- **DELETE:** Lines 32-38
|
||||
- **REPLACE WITH:** Single line `const targetRoute = computed(() => '/dashboard')`
|
||||
|
||||
### Verification
|
||||
1. Log in as any user
|
||||
2. Navigate to `/organization/:id/vehicles`
|
||||
3. Click the `HeaderLogo` → must navigate to `/dashboard` (not stay on org pages)
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 2: Backend 500 Error — `float(None)` on Wallet Balance
|
||||
|
||||
### File
|
||||
`backend/app/services/billing_engine.py` — `AtomicTransactionManager.get_wallet_summary()`
|
||||
|
||||
### Current Behavior (Lines 622-635)
|
||||
```python
|
||||
return {
|
||||
"wallet_id": wallet.id,
|
||||
"balances": {
|
||||
"earned": float(wallet.earned_credits), # 💥 CRASHES if None
|
||||
"purchased": float(wallet.purchased_credits), # 💥 CRASHES if None
|
||||
"service_coins": float(wallet.service_coins), # 💥 CRASHES if None
|
||||
"voucher": float(voucher_balance),
|
||||
"total": float(
|
||||
wallet.earned_credits + # 💥 CRASHES if None
|
||||
wallet.purchased_credits +
|
||||
wallet.service_coins +
|
||||
voucher_balance
|
||||
)
|
||||
},
|
||||
```
|
||||
|
||||
### Root Cause
|
||||
The `Wallet` model defines credit columns as:
|
||||
```python
|
||||
earned_credits: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
|
||||
```
|
||||
|
||||
`server_default` only applies at the DB level on INSERT. If a wallet row was created with explicit `None` values, or if the DB column contains `NULL` (e.g., from a migration or manual SQL), then `wallet.earned_credits` will be Python `None`. Calling `float(None)` raises `TypeError`, which falls through to the generic `Exception` handler returning HTTP 500.
|
||||
|
||||
Also: if the wallet row does NOT exist at all, `get_wallet_summary()` raises `ValueError(f"Wallet not found for user_id={user_id}")` → which IS caught as 404. So the 500 specifically means: **wallet row EXISTS but has NULL credit columns**.
|
||||
|
||||
### Fix
|
||||
|
||||
**Option A (Defensive — Preferred): Wrap all `float()` calls with `or 0`**
|
||||
|
||||
Change lines 625-634 to:
|
||||
```python
|
||||
"earned": float(wallet.earned_credits or 0),
|
||||
"purchased": float(wallet.purchased_credits or 0),
|
||||
"service_coins": float(wallet.service_coins or 0),
|
||||
"voucher": float(voucher_balance or 0),
|
||||
"total": float(
|
||||
(wallet.earned_credits or 0) +
|
||||
(wallet.purchased_credits or 0) +
|
||||
(wallet.service_coins or 0) +
|
||||
(voucher_balance or 0)
|
||||
)
|
||||
```
|
||||
|
||||
**Option B (Root Fix — also recommended): Run a DB repair script to fix NULL columns**
|
||||
|
||||
```sql
|
||||
UPDATE identity.wallets SET earned_credits = 0 WHERE earned_credits IS NULL;
|
||||
UPDATE identity.wallets SET purchased_credits = 0 WHERE purchased_credits IS NULL;
|
||||
UPDATE identity.wallets SET service_coins = 0 WHERE service_coins IS NULL;
|
||||
```
|
||||
|
||||
**BOTH Option A and Option B should be applied.**
|
||||
|
||||
### Verification
|
||||
1. Find NULL wallets: `SELECT id, user_id FROM identity.wallets WHERE earned_credits IS NULL OR purchased_credits IS NULL OR service_coins IS NULL;`
|
||||
2. Apply both fixes
|
||||
3. GET /api/v1/billing/wallet/balance with JWT for user 106 → must return 200
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 3: "705,000" Mock Data — NOT a Code Issue
|
||||
|
||||
### Finding
|
||||
**No mock/hardcoded data was found in any frontend file.** Searched all .vue, .ts, and .json files for 705000 — zero results.
|
||||
|
||||
The finance store properly fetches from the API and has zero-fallback:
|
||||
```typescript
|
||||
const totalCredits = computed(() => balance.value?.total ?? 0)
|
||||
```
|
||||
|
||||
The "705,000" value is from a real database record — likely the admin test user's wallet. User 106 sees an error because their wallet has NULL credits. Once Bug 2 is fixed, user 106 will see 0 (either no wallet → creates one, or NULL columns → returned as 0).
|
||||
|
||||
### Action
|
||||
**No code change needed for Bug 3.** Fixing Bug 2 resolves the 500 error cascade.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 4: Missing i18n Keys in SubscriptionStatusWidget
|
||||
|
||||
### File
|
||||
`frontend_app/src/components/dashboard/SubscriptionStatusWidget.vue`
|
||||
|
||||
### Missing Keys (Lines 52, 53, 67, 81)
|
||||
- `subscription.vehicles` (Line 67, 81)
|
||||
- `subscription.unlimited` (Line 81)
|
||||
- `subscription.timeRemaining` (Line 52)
|
||||
- `subscription.days` (Line 53)
|
||||
|
||||
None of these exist in `hu.ts` or `en.ts`.
|
||||
|
||||
### Fix
|
||||
Add to BOTH `frontend_app/src/i18n/hu.ts` AND `frontend_app/src/i18n/en.ts` inside the `subscription` block, after `indefinite`:
|
||||
|
||||
**hu.ts — add:**
|
||||
```typescript
|
||||
vehicles: 'jármű',
|
||||
unlimited: 'korlátlan',
|
||||
timeRemaining: 'Hátralévő idő',
|
||||
days: 'nap',
|
||||
```
|
||||
|
||||
**en.ts — add:**
|
||||
```typescript
|
||||
vehicles: 'vehicles',
|
||||
unlimited: 'unlimited',
|
||||
timeRemaining: 'Time Remaining',
|
||||
days: 'days',
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Execution Order (Code Mode)
|
||||
|
||||
1. **Bug 1** — HeaderLogo.vue: Change targetRoute to always return /dashboard
|
||||
2. **Bug 4** — hu.ts + en.ts: Add 4 missing i18n keys
|
||||
3. **Bug 2** — billing_engine.py: Add `or 0` to all float() conversions
|
||||
4. **Bug 2 (DB)** — Run SQL to NULL→0 on identity.wallets credit columns
|
||||
5. **Bug 3** — Verify fix, no code change needed
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Files Summary
|
||||
|
||||
| File | Bug | Operation |
|
||||
|------|-----|-----------|
|
||||
| frontend_app/src/components/header/HeaderLogo.vue | #1 | Replace lines 32-38 |
|
||||
| frontend_app/src/i18n/hu.ts | #4 | Add 4 keys after line 83 |
|
||||
| frontend_app/src/i18n/en.ts | #4 | Add 4 keys after line 83 |
|
||||
| backend/app/services/billing_engine.py | #2 | Modify lines 625-634 |
|
||||
| identity.wallets (DB) | #2 | SQL UPDATE NULL→0 |
|
||||
|
||||
---
|
||||
|
||||
*Blueprint ready for Code Mode hand-off.*
|
||||
307
plans/finance_layout_clone_and_enum_fix_2026-07-26.md
Normal file
307
plans/finance_layout_clone_and_enum_fix_2026-07-26.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# 🔵 Finance Module — FleetView Layout Clone + Enum Fix Blueprint
|
||||
|
||||
**Date:** 2026-07-26
|
||||
**Author:** Architect Mode
|
||||
**Status:** Pending Code Mode Execution
|
||||
**Priority:** MEDIUM — Layout fix and enum data repair
|
||||
|
||||
---
|
||||
|
||||
## STEP 1: THE WORKING LAYOUT — FleetView Analysis
|
||||
|
||||
**Route:** `/organization/:org_id/vehicles`
|
||||
**Component:** [`FleetView.vue`](frontend_app/src/views/vehicles/FleetView.vue) wrapped in [`OrganizationLayout.vue`](frontend_app/src/layouts/OrganizationLayout.vue)
|
||||
|
||||
### FleetView.vue DOM Hierarchy (Key Elements)
|
||||
|
||||
```html
|
||||
<div class="relative min-h-screen">
|
||||
<!-- Fixed background -->
|
||||
<div class="fixed inset-0 z-0">
|
||||
<div class="absolute inset-0 bg-[url('@/assets/garage-bg.png')] bg-cover bg-center bg-fixed"></div>
|
||||
</div>
|
||||
<!-- Dark overlay -->
|
||||
<div class="fixed inset-0 z-0 bg-slate-900/65 pointer-events-none"></div>
|
||||
|
||||
<!-- ═══ MAIN CONTENT CONTAINER ═══ -->
|
||||
<div class="relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<!-- Page header: flex justify-between -->
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">{{ title }}</h1>
|
||||
<p class="mt-1 text-sm text-white/60">{{ subtitle }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Action buttons + Back button -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Cards -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### OrganizationLayout.vue Role
|
||||
|
||||
[`OrganizationLayout.vue`](frontend_app/src/layouts/OrganizationLayout.vue) provides:
|
||||
- `BaseHeader` with company name + hamburger menu
|
||||
- `main class="relative z-10"` → `<router-view />`
|
||||
- **NO background** — the child view (`FleetView`) provides its own
|
||||
|
||||
### FinanceLayout.vue Current State
|
||||
|
||||
[`FinanceLayout.vue`](frontend_app/src/layouts/FinanceLayout.vue) provides:
|
||||
- `BaseHeader` with `HeaderLogo` + `LanguageSwitcher` + `HeaderProfile`
|
||||
- **ALREADY HAS background** — `fixed inset-0 z-0` + `bg-[url('@/assets/garage-bg.png')]` + dark overlay `bg-black/30`
|
||||
- `main class="relative z-10"` → `<router-view />`
|
||||
|
||||
### Critical Difference: FinanceLayout vs OrganizationLayout
|
||||
|
||||
| Property | OrganizationLayout | FinanceLayout |
|
||||
|----------|-------------------|---------------|
|
||||
| Background | ❌ None (child provides) | ✅ Already provides `garage-bg.png` |
|
||||
| Dark overlay | ❌ None | ✅ Already provides `bg-black/30` |
|
||||
| Header | Company name + hamburger | Logo + lang switcher + profile |
|
||||
| `main` class | `relative z-10` | `relative z-10` |
|
||||
|
||||
**Conclusion:** Since `FinanceLayout` already provides the background + overlay, the child views should NOT duplicate them. The fix is to make `FinanceMainView.vue` use the **same inner container pattern** as `FleetView` (line 12: `relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8`) but SKIP the background/overlay divs (which are in FinanceLayout).
|
||||
|
||||
### What's Currently Wrong with FinanceMainView
|
||||
|
||||
Current structure (after previous fixes):
|
||||
```html
|
||||
<!-- FinanceMainView.vue template: -->
|
||||
<div class="flex flex-col justify-end min-h-[85vh] pb-8">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full">
|
||||
<div class="mb-8 flex items-center justify-between"> ...header... </div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4"> ...cards... </div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Problems:
|
||||
1. **`justify-end min-h-[85vh]`** — This is the DashboardView pattern (bottom-aligned). FleetView uses `py-8` (top-aligned). The finance cards should be top-aligned with proper padding.
|
||||
2. **`lg:grid-cols-5`** — Dashboard uses 5-column for its 5 cards. Finance has 4 cards. Should use whatever fits well.
|
||||
3. No `relative` on outer wrapper — needed for z-index context.
|
||||
|
||||
---
|
||||
|
||||
## STEP 2: THE FIX — Clone FleetView Inner Structure
|
||||
|
||||
For [`FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue):
|
||||
|
||||
```html
|
||||
<template>
|
||||
<div class="relative min-h-screen">
|
||||
<!-- ═══ MAIN CONTENT CONTAINER (FleetView.vue line 12 clone) ═══ -->
|
||||
<div class="relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<!-- ── Page header ── -->
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">💰 {{ t('menu.finance') }}</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="router.push('/dashboard')"
|
||||
class="flex items-center gap-2 rounded-xl bg-white/10 px-4 py-2 text-sm text-white/80 transition-all duration-200 hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
{{ t('garage.backToDashboard') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 4-Card Grid ── -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
...cards...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Key changes:**
|
||||
1. Outer wrapper: `<div class="relative min-h-screen">` (replaces `flex flex-col justify-end min-h-[85vh] pb-8`)
|
||||
2. Content container: `<div class="relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">` — exact clone of FleetView:12
|
||||
3. Header stays nested inside the `mx-auto` container
|
||||
4. Grid: `lg:grid-cols-4` instead of `lg:grid-cols-5` (4 cards instead of 5)
|
||||
5. NO background divs (FinanceLayout provides them)
|
||||
|
||||
### Same fix for other 3 finance views
|
||||
|
||||
[`FinanceWalletsView.vue`](frontend_app/src/views/FinanceWalletsView.vue), [`FinancePackagesView.vue`](frontend_app/src/views/FinancePackagesView.vue), [`FinanceTransactionsView.vue`](frontend_app/src/views/FinanceTransactionsView.vue) — each needs the same structural fix: remove `flex flex-col justify-start min-h-[85vh] py-8` wrapper and replace with `relative min-h-screen` + `relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8`.
|
||||
|
||||
---
|
||||
|
||||
## STEP 3: LEDGER_STATUS ENUM FIX
|
||||
|
||||
### 3A: The Enum Definition
|
||||
|
||||
[`audit.py`](backend/app/models/system/audit.py:70-75):
|
||||
```python
|
||||
class LedgerStatus(str, enum.Enum):
|
||||
PENDING = "PENDING"
|
||||
SUCCESS = "SUCCESS"
|
||||
FAILED = "FAILED"
|
||||
REFUNDED = "REFUNDED"
|
||||
REFUND = "REFUND"
|
||||
```
|
||||
|
||||
### 3B: The Seed Script (BROKEN)
|
||||
|
||||
[`seed_financial_ledger.py:20`](backend/scripts/seed_financial_ledger.py:20):
|
||||
```python
|
||||
DB_STATUSES = ["pending", "completed", "failed", "cancelled"]
|
||||
```
|
||||
|
||||
Mapping to valid enum values:
|
||||
| Seed value | Valid Enum | Reason |
|
||||
|-----------|-----------|--------|
|
||||
| `pending` → | `PENDING` | Case fix only |
|
||||
| `completed` → | `SUCCESS` | Closest semantic match (completed payment = success) |
|
||||
| `failed` → | `FAILED` | Case fix only |
|
||||
| `cancelled` → | `REFUNDED` | Cancelled payments are typically refunded |
|
||||
|
||||
### 3C: Fix Plan
|
||||
|
||||
**Fix A — Seed script:**
|
||||
```python
|
||||
DB_STATUSES = ["PENDING", "SUCCESS", "FAILED", "REFUNDED"]
|
||||
```
|
||||
|
||||
**Fix B — Database cleanup SQL:**
|
||||
```sql
|
||||
UPDATE audit.financial_ledger SET status = 'PENDING' WHERE status::text = 'pending';
|
||||
UPDATE audit.financial_ledger SET status = 'SUCCESS' WHERE status::text = 'completed';
|
||||
UPDATE audit.financial_ledger SET status = 'FAILED' WHERE status::text = 'failed';
|
||||
UPDATE audit.financial_ledger SET status = 'REFUNDED' WHERE status::text = 'cancelled';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## STEP 4: CODE MODE HAND-OFF — Task List
|
||||
|
||||
### Task 1: Restructure FinanceMainView layout (FleetView clone)
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue)
|
||||
|
||||
Replace the entire `<template>` section. The current outer structure:
|
||||
```html
|
||||
<div class="flex flex-col justify-end min-h-[85vh] pb-8">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full">
|
||||
<!-- header inside container (already correct from previous fix) -->
|
||||
<!-- grid: lg:grid-cols-5 -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```html
|
||||
<div class="relative min-h-screen">
|
||||
<div class="relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<!-- header (same as current, already correct) -->
|
||||
<!-- grid: change lg:grid-cols-5 → lg:grid-cols-4 -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
1. Change the outermost `<div>`: remove `flex flex-col justify-end min-h-[85vh] pb-8` → add `relative min-h-screen`
|
||||
2. Change the content container: `mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full` → `relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8` (add `relative z-10` and `py-8`, remove `w-full`)
|
||||
3. Change all card grid: `lg:grid-cols-5` → `lg:grid-cols-4` (4 cards, not 5)
|
||||
4. Also change Card 1: remove `lg:col-span-2` since on a 4-col grid each card gets 1 column
|
||||
|
||||
### Task 2: Restructure FinanceWalletsView layout
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceWalletsView.vue`](frontend_app/src/views/FinanceWalletsView.vue)
|
||||
|
||||
Same structural change:
|
||||
- Outer: `flex flex-col justify-start min-h-[85vh] py-8` → `relative min-h-screen`
|
||||
- Inner content: `mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full` → `relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8`
|
||||
|
||||
### Task 3: Restructure FinancePackagesView layout
|
||||
|
||||
**File:** [`frontend_app/src/views/FinancePackagesView.vue`](frontend_app/src/views/FinancePackagesView.vue)
|
||||
|
||||
Same structural change as Task 2.
|
||||
|
||||
### Task 4: Restructure FinanceTransactionsView layout
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceTransactionsView.vue`](frontend_app/src/views/FinanceTransactionsView.vue)
|
||||
|
||||
Same structural change as Task 2.
|
||||
|
||||
### Task 5: Fix seed_financial_ledger.py ledger_status enum (3A)
|
||||
|
||||
**File:** [`backend/scripts/seed_financial_ledger.py:20`](backend/scripts/seed_financial_ledger.py:20)
|
||||
|
||||
Change:
|
||||
```python
|
||||
DB_STATUSES = ["pending", "completed", "failed", "cancelled"]
|
||||
```
|
||||
To:
|
||||
```python
|
||||
DB_STATUSES = ["PENDING", "SUCCESS", "FAILED", "REFUNDED"]
|
||||
```
|
||||
|
||||
### Task 6: Fix existing DB ledger_status bad data (3B)
|
||||
|
||||
Run SQL:
|
||||
```bash
|
||||
docker compose exec shared-postgres psql -U service_finder_app -d service_finder -c "
|
||||
UPDATE audit.financial_ledger SET status = 'PENDING' WHERE status::text = 'pending';
|
||||
UPDATE audit.financial_ledger SET status = 'SUCCESS' WHERE status::text = 'completed';
|
||||
UPDATE audit.financial_ledger SET status = 'FAILED' WHERE status::text = 'failed';
|
||||
UPDATE audit.financial_ledger SET status = 'REFUNDED' WHERE status::text = 'cancelled';
|
||||
SELECT status::text, COUNT(*) as cnt FROM audit.financial_ledger GROUP BY status ORDER BY status;
|
||||
"
|
||||
```
|
||||
|
||||
### Task 7: Full verification
|
||||
|
||||
1. **Frontend build:**
|
||||
```bash
|
||||
docker compose exec sf_public_frontend npm run build 2>&1 | tail -5
|
||||
```
|
||||
|
||||
2. **Backend wallet balance API (no enum crash):**
|
||||
```bash
|
||||
docker compose exec sf_api python3 -c "
|
||||
import requests, json
|
||||
r = requests.post('http://localhost:8000/api/v1/auth/login', data={'username': 'admin@profibot.hu', 'password': 'Admin123!'})
|
||||
token = r.json()['access_token']
|
||||
resp = requests.get('http://localhost:8000/api/v1/billing/wallet/balance', headers={'Authorization': f'Bearer {token}'})
|
||||
print(f'Status: {resp.status_code}')
|
||||
if resp.status_code == 200:
|
||||
d = resp.json()
|
||||
print(f'total={d.get(\"total\")} earned={d.get(\"earned\")}')
|
||||
else:
|
||||
print(f'ERROR: {resp.text}')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Summary
|
||||
|
||||
| # | Task | File(s) |
|
||||
|---|------|---------|
|
||||
| 1 | Clone FleetView layout to FinanceMainView | [`FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue) |
|
||||
| 2 | Clone FleetView layout to FinanceWalletsView | [`FinanceWalletsView.vue`](frontend_app/src/views/FinanceWalletsView.vue) |
|
||||
| 3 | Clone FleetView layout to FinancePackagesView | [`FinancePackagesView.vue`](frontend_app/src/views/FinancePackagesView.vue) |
|
||||
| 4 | Clone FleetView layout to FinanceTransactionsView | [`FinanceTransactionsView.vue`](frontend_app/src/views/FinanceTransactionsView.vue) |
|
||||
| 5 | Fix seed enum: `ledger_status` | [`seed_financial_ledger.py:20`](backend/scripts/seed_financial_ledger.py:20) |
|
||||
| 6 | Fix DB: normalize `ledger_status` values | PostgreSQL `audit.financial_ledger` |
|
||||
| 7 | Verify: build + API | Build + `/billing/wallet/balance` |
|
||||
|
||||
## ⚠️ DO NOT MODIFY
|
||||
|
||||
- [`FleetView.vue`](frontend_app/src/views/vehicles/FleetView.vue) — reference pattern
|
||||
- [`OrganizationLayout.vue`](frontend_app/src/layouts/OrganizationLayout.vue) — reference pattern
|
||||
- [`FinanceLayout.vue`](frontend_app/src/layouts/FinanceLayout.vue) — already correct (provides background)
|
||||
- [`BaseHeader.vue`](frontend_app/src/components/layout/BaseHeader.vue) — working
|
||||
- [`router/index.ts`](frontend_app/src/router/index.ts) — correct
|
||||
287
plans/finance_mock_data_and_back_button_blueprint_2026-07-26.md
Normal file
287
plans/finance_mock_data_and_back_button_blueprint_2026-07-26.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# 🔵 Finance Module — Mock Data & Back Button Blueprint
|
||||
|
||||
**Date:** 2026-07-26
|
||||
**Author:** Architect Mode
|
||||
**Status:** Pending Code Mode Execution
|
||||
**Priority:** MEDIUM — UI polish, no app-breaking bugs
|
||||
|
||||
---
|
||||
|
||||
## STEP 1: MOCK DATA ANALYSIS
|
||||
|
||||
### 1.1 Dashboard Financial Card (ProfileTrustCard.vue)
|
||||
|
||||
**File:** [`frontend_app/src/components/dashboard/ProfileTrustCard.vue`](frontend_app/src/components/dashboard/ProfileTrustCard.vue)
|
||||
|
||||
| Field | Current Value | Source | Is Mock? |
|
||||
|-------|--------------|--------|----------|
|
||||
| `financeStore.totalCredits` (line 40) | Live API data | `financeStore.fetchWalletBalance()` → `GET /billing/wallet/balance` | **❌ LIVE** — already fetches real data |
|
||||
| `financeStore.serviceCoins` (line 43) | Live API data | Same store | **❌ LIVE** |
|
||||
| `financeStore.totalCredits` stat tile (line 51) | Live API data | Same store | **❌ LIVE** |
|
||||
| Transaction count (line 57) | `—` hardcoded | Static placeholder | **✅ MOCK** |
|
||||
| Package info (line 63) | `•` hardcoded | Static placeholder | **✅ MOCK** |
|
||||
| `financeStore.voucherBalance` (line 69) | Live API data | Same store | **❌ LIVE** |
|
||||
|
||||
**Conclusion:** The wallet balance numbers (`totalCredits`, `serviceCoins`, `voucherBalance`) ARE live and fetched from the API. However, two stat tiles show static placeholders that render identically for all users.
|
||||
|
||||
### 1.2 Gamification Card (GamificationCard.vue)
|
||||
|
||||
**File:** [`frontend_app/src/components/dashboard/GamificationCard.vue`](frontend_app/src/components/dashboard/GamificationCard.vue)
|
||||
|
||||
| Field | Current Value | Source | Is Mock? |
|
||||
|-------|--------------|--------|----------|
|
||||
| Score (line 18) | `2,450` hardcoded | Literal number in template | **✅ MOCK** — same for all users |
|
||||
| Badges (lines 56-61) | 4 hardcoded entries | Static array in script | **✅ MOCK** — same for all users |
|
||||
|
||||
**Evidence:**
|
||||
- [`GamificationCard.vue`](frontend_app/src/components/dashboard/GamificationCard.vue:18): `<p class="text-3xl font-extrabold text-emerald-600">2,450</p>` — literal HTML
|
||||
- [`GamificationCard.vue`](frontend_app/src/components/dashboard/GamificationCard.vue:56-61): `const badges = ref([{ icon: '🌱', label: 'Eco Driver' }, ...])` — never updated from API
|
||||
|
||||
**Fix plan:**
|
||||
- Import a gamification/Pinía store (check if one exists) to fetch real user score
|
||||
- If no gamification store exists yet, create a computed that reads from `authStore` or call an API endpoint
|
||||
- Replace hardcoded badges with data fetched from a gamification endpoint
|
||||
|
||||
### 1.3 FinanceMainView Cards
|
||||
|
||||
**File:** [`frontend_app/src/components/../views/FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue)
|
||||
|
||||
**Card 1 (Wallet):** Uses `financeStore.totalCredits`, `financeStore.purchasedCredits`, `financeStore.serviceCoins`, `financeStore.earnedCredits`, `financeStore.voucherBalance` — all live data from `financeStore.fetchWalletBalance()` ✅
|
||||
|
||||
**Card 2 (Packages):** Uses `authStore.user?.subscription_plan` — live data ✅
|
||||
|
||||
**Card 3 (Transactions):** Static placeholder with emoji — by design (placeholder page) ✅
|
||||
|
||||
**Card 4 (Invoices):** Static placeholder with 🚧 — by design ("Hamarosan") ✅
|
||||
|
||||
---
|
||||
|
||||
## STEP 2: BACK BUTTON LAYOUT ANALYSIS
|
||||
|
||||
### 2.1 The Gold Standard: FleetView.vue Header
|
||||
|
||||
**File:** [`frontend_app/src/views/vehicles/FleetView.vue`](frontend_app/src/views/vehicles/FleetView.vue:13-57)
|
||||
|
||||
The FleetView header pattern:
|
||||
|
||||
```html
|
||||
<!-- Page header -->
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">{{ t('garage.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-white/60">{{ t('garage.subtitle', { count: ... }) }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Action buttons on the right -->
|
||||
<button ...>Add Vehicle</button>
|
||||
<button @click="router.push('/dashboard')" class="...bg-white/10...">
|
||||
<svg>...</svg>
|
||||
{{ t('garage.backToDashboard') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Key structural elements:**
|
||||
1. Container: `class="mb-8 flex items-center justify-between"`
|
||||
2. Left side: `<div>` with `<h1>` title + optional `<p>` subtitle
|
||||
3. Right side: `<div class="flex items-center gap-3">` with action buttons
|
||||
4. Back button: `bg-white/10 px-4 py-2 text-sm text-white/80 hover:bg-white/20 hover:text-white` + left-arrow SVG
|
||||
|
||||
### 2.2 Current Finance Views: Teleport Anti-Pattern
|
||||
|
||||
All 4 finance views currently use `<Teleport to="#header-teleport-target">` to inject a page title into the global header:
|
||||
|
||||
- [`FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue:38-40): `<Teleport v-if="teleportReady" to="#header-teleport-target">`
|
||||
- [`FinanceWalletsView.vue`](frontend_app/src/views/FinanceWalletsView.vue:19-21)
|
||||
- [`FinancePackagesView.vue`](frontend_app/src/views/FinancePackagesView.vue:17-19)
|
||||
- [`FinanceTransactionsView.vue`](frontend_app/src/views/FinanceTransactionsView.vue:15-17)
|
||||
|
||||
All 4 need the same structural change.
|
||||
|
||||
### 2.3 The Fix: FleetView Pattern Applied to Finance
|
||||
|
||||
For each finance view:
|
||||
|
||||
1. **REMOVE** the entire `<Teleport ...>` block
|
||||
2. **REMOVE** `teleportReady`, `onMounted`, `onBeforeUnmount` boilerplate
|
||||
3. **ADD** a local content header matching FleetView:
|
||||
```html
|
||||
<!-- Page header -->
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">💰 {{ t('menu.finance') }}</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="router.push('/dashboard')"
|
||||
class="flex items-center gap-2 rounded-xl bg-white/10 px-4 py-2 text-sm text-white/80 transition-all duration-200 hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
{{ t('garage.backToDashboard') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
4. **PRESERVE** the existing card grid / content area below the header
|
||||
|
||||
---
|
||||
|
||||
## STEP 3: CODE MODE HAND-OFF — Explicit Task List
|
||||
|
||||
Execute in order. Do NOT modify FleetView.vue, PrivateLayout.vue, BaseHeader.vue, or router/index.ts.
|
||||
|
||||
### Task A: Fix Mock Data in GamificationCard.vue
|
||||
|
||||
**File:** [`frontend_app/src/components/dashboard/GamificationCard.vue`](frontend_app/src/components/dashboard/GamificationCard.vue)
|
||||
|
||||
1. **Check if a gamification store exists:**
|
||||
```bash
|
||||
ls frontend_app/src/stores/gamification* 2>/dev/null || echo "No gamification store"
|
||||
```
|
||||
If a store exists, import and use it. If not, create a simple onMounted fetch to a gamification endpoint.
|
||||
|
||||
2. **Replace hardcoded score (line 18):**
|
||||
- Change `<p class="text-3xl font-extrabold text-emerald-600">2,450</p>`
|
||||
- To: `<p class="text-3xl font-extrabold text-emerald-600">{{ userScore }}</p>`
|
||||
- Add script logic: `const userScore = computed(...)` linked to real data source
|
||||
|
||||
3. **Replace hardcoded badges (lines 56-61):**
|
||||
- Change the static `badges` array to a computed or ref that fetches from API
|
||||
- Keep the same HTML template rendering — just change data source
|
||||
|
||||
### Task B: Fix Mock Data in ProfileTrustCard.vue
|
||||
|
||||
**File:** [`frontend_app/src/components/dashboard/ProfileTrustCard.vue`](frontend_app/src/components/dashboard/ProfileTrustCard.vue)
|
||||
|
||||
1. **Fix transaction count (line 57):**
|
||||
- Change `<p class="text-lg font-bold text-slate-800">—</p>`
|
||||
- To: `<p class="text-lg font-bold text-slate-800">{{ transactionCount }}</p>`
|
||||
- Add: `const transactionCount = computed(() => 0)` — placeholder for now, will wire to real API later
|
||||
|
||||
2. **Fix package info (line 63):**
|
||||
- Change `<p class="text-lg font-bold text-slate-800">•</p>`
|
||||
- To: `<p class="text-lg font-bold text-slate-800">{{ authStore.user?.subscription_plan || '—' }}</p>`
|
||||
- Add: `import { useAuthStore } from '../../stores/auth'` and `const authStore = useAuthStore()`
|
||||
|
||||
### Task C: Remove Teleport & Add Local Header — FinanceMainView.vue
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue)
|
||||
|
||||
1. **DELETE lines 38-40** — the entire Teleport block:
|
||||
```html
|
||||
<Teleport v-if="teleportReady" to="#header-teleport-target">
|
||||
<span class="text-white font-bold text-sm tracking-wide">💰 {{ t('menu.finance') }}</span>
|
||||
</Teleport>
|
||||
```
|
||||
|
||||
2. **ADD local content header** immediately after the Teleport block removal (before line 42's `<div class="flex flex-col...">`):
|
||||
```html
|
||||
<!-- ── Page header (FleetView.vue pattern) ── -->
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">💰 {{ t('menu.finance') }}</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="router.push('/dashboard')"
|
||||
class="flex items-center gap-2 rounded-xl bg-white/10 px-4 py-2 text-sm text-white/80 transition-all duration-200 hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
{{ t('garage.backToDashboard') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
3. **Clean up script:** Remove `teleportReady`, `ref`, and lifecycle hooks that are no longer needed:
|
||||
- Line 237: Change `import { computed, ref, onMounted, onBeforeUnmount } from 'vue'` to `import { computed, onMounted } from 'vue'`
|
||||
- Line 243: Delete `const teleportReady = ref(false)`
|
||||
- Line 278: Delete `teleportReady.value = true`
|
||||
- Lines 282-284: Delete `onBeforeUnmount` block
|
||||
|
||||
### Task D: Remove Teleport & Add Local Header — FinanceWalletsView.vue
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceWalletsView.vue`](frontend_app/src/views/FinanceWalletsView.vue)
|
||||
|
||||
1. **DELETE lines 19-21** — the Teleport block
|
||||
2. **ADD** local content header (after line 21 deletion, before line 23's `<div class="flex flex-col...">`):
|
||||
```html
|
||||
<!-- ── Page header (FleetView.vue pattern) ── -->
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">💰 {{ t('finance.wallet') }}</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="$router.push('/finance')"
|
||||
class="flex items-center gap-2 rounded-xl bg-white/10 px-4 py-2 text-sm text-white/80 transition-all duration-200 hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
{{ t('common.back') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
(Note: back button goes to `/finance` for sub-pages)
|
||||
|
||||
3. **Clean up script:** Change imports: remove `ref, onMounted, onBeforeUnmount`, remove `teleportReady` and lifecycle hooks.
|
||||
- Also add: `import { useRouter } from 'vue-router'` and `const router = useRouter()` for the back button (or use `$router` in template)
|
||||
|
||||
### Task E: Remove Teleport & Add Local Header — FinancePackagesView.vue
|
||||
|
||||
**File:** [`frontend_app/src/views/FinancePackagesView.vue`](frontend_app/src/views/FinancePackagesView.vue)
|
||||
|
||||
1. **DELETE lines 17-19** — the Teleport block
|
||||
2. **ADD** local content header with title "📦 {{ t('subscription.title') }}" and back button to `/finance`
|
||||
3. **Clean up script:** Same as Task D — remove teleport boilerplate, add router import if not present
|
||||
|
||||
### Task F: Remove Teleport & Add Local Header — FinanceTransactionsView.vue
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceTransactionsView.vue`](frontend_app/src/views/FinanceTransactionsView.vue)
|
||||
|
||||
1. **DELETE lines 15-17** — the Teleport block
|
||||
2. **ADD** local content header with title "📋 {{ t('finance.transactionHistory') }}" and back button to `/finance`
|
||||
3. **Clean up script:** Same as Task D — remove teleport boilerplate, add router import if not present
|
||||
|
||||
### Task G: Verify
|
||||
|
||||
1. **Frontend build check:**
|
||||
```bash
|
||||
docker compose exec sf_public_frontend npm run build 2>&1 | tail -5
|
||||
```
|
||||
Must show `✓ built in X.XXs` with no errors.
|
||||
|
||||
2. **Backend imports:**
|
||||
```bash
|
||||
docker compose exec sf_api python3 -c "from app.main import app; print('OK')"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Summary Table
|
||||
|
||||
| Task | File | Action |
|
||||
|------|------|--------|
|
||||
| A | [`GamificationCard.vue`](frontend_app/src/components/dashboard/GamificationCard.vue) | Replace `2,450` with live score, replace hardcoded badges |
|
||||
| B | [`ProfileTrustCard.vue`](frontend_app/src/components/dashboard/ProfileTrustCard.vue) | Replace `—` and `•` with live/computed values |
|
||||
| C | [`FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue) | Remove Teleport, add FleetView-style local header + back button |
|
||||
| D | [`FinanceWalletsView.vue`](frontend_app/src/views/FinanceWalletsView.vue) | Remove Teleport, add local header + back button to `/finance` |
|
||||
| E | [`FinancePackagesView.vue`](frontend_app/src/views/FinancePackagesView.vue) | Remove Teleport, add local header + back button to `/finance` |
|
||||
| F | [`FinanceTransactionsView.vue`](frontend_app/src/views/FinanceTransactionsView.vue) | Remove Teleport, add local header + back button to `/finance` |
|
||||
| G | Build + verify | `npm run build`, backend imports check |
|
||||
|
||||
## ⚠️ DO NOT MODIFY
|
||||
|
||||
- [`FleetView.vue`](frontend_app/src/views/vehicles/FleetView.vue) — reference pattern only
|
||||
- [`FinanceLayout.vue`](frontend_app/src/layouts/FinanceLayout.vue) — layout is correct
|
||||
- [`BaseHeader.vue`](frontend_app/src/components/layout/BaseHeader.vue) — working header
|
||||
- [`router/index.ts`](frontend_app/src/router/index.ts) — correct route config
|
||||
- [`DashboardView.vue`](frontend_app/src/views/DashboardView.vue) — working dashboard
|
||||
257
plans/finance_module_corrective_blueprint_2026-07-26.md
Normal file
257
plans/finance_module_corrective_blueprint_2026-07-26.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# 🔴 Finance Module — Corrective Blueprint (4 Failures)
|
||||
|
||||
**Date:** 2026-07-26
|
||||
**Author:** Architect Mode
|
||||
**Status:** Pending Code Mode Execution
|
||||
**Priority:** HIGH — Visual misalignment, duplicate back button, mock data, backend enum crash
|
||||
|
||||
---
|
||||
|
||||
## STEP 1: FAILURE ANALYSIS
|
||||
|
||||
### FAILURE 1: Wrong Dashboard Card — Mock Data Remains
|
||||
|
||||
**Finding:** The dashboard at [`DashboardView.vue`](frontend_app/src/views/DashboardView.vue:72) uses [`ProfileTrustCard`](frontend_app/src/components/dashboard/ProfileTrustCard.vue) (Card 5). This IS the correct card and it WAS correctly modified by the previous blueprint — it now fetches live `financeStore` data and shows `subscriptionPlanName` from `authStore`.
|
||||
|
||||
However, [`FinancialCard.vue`](frontend_app/src/components/dashboard/FinancialCard.vue) is a **separate, unused** component that also fetches live wallet data and has a "Top Up" button. It is NOT imported or rendered anywhere on the dashboard. It appears to be a legacy/standalone variant.
|
||||
|
||||
**The actual source of "mock data" on the dashboard is the `transactionCount` computed (always 0) and the `subscriptionPlanName` which shows `—` if the user has no subscription plan set.**
|
||||
|
||||
**Fix plan:**
|
||||
- The `ProfileTrustCard` correctly fetches live `financeStore` data already.
|
||||
- The `GamificationCard` at [`GamificationCard.vue`](frontend_app/src/components/dashboard/GamificationCard.vue:18) was changed from hardcoded `2,450` to `computed(() => 0)` — specifically requested by the previous blueprint as a "placeholder ready to wire up."
|
||||
- If the user sees mock data, it's the gamification score (0) and badges (empty array). These ARE mock/placeholder by design until a gamification store is created.
|
||||
|
||||
**No additional changes needed for the dashboard card itself.**
|
||||
|
||||
### FAILURE 2: Back Button Still in Top Global Navbar
|
||||
|
||||
**Root Cause:** The previous blueprint removed `<Teleport>` from all 4 child views BUT did NOT remove `<HeaderBackButton>` from [`FinanceLayout.vue`](frontend_app/src/layouts/FinanceLayout.vue:42-45).
|
||||
|
||||
**Evidence:**
|
||||
```html
|
||||
<!-- FinanceLayout.vue:42-45 -->
|
||||
<HeaderBackButton
|
||||
:to="backTarget"
|
||||
:label="t('common.back')"
|
||||
/>
|
||||
```
|
||||
This `HeaderBackButton` is rendered in the `#left` slot of `BaseHeader`, which is the global sticky top navbar. Removing the Teleport only removed the page title — the back button in the layout persists.
|
||||
|
||||
**Fix:** Delete lines 42-45 from [`FinanceLayout.vue`](frontend_app/src/layouts/FinanceLayout.vue). Also remove the unused `backTarget` computed (lines 95-100) and clean up the `useRoute` import if no longer needed.
|
||||
|
||||
### FAILURE 3: Title Misalignment — Header Outside Content Container
|
||||
|
||||
**Root Cause:** The newly added local header in [`FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue:39-55) is placed OUTSIDE the main content container.
|
||||
|
||||
**Evidence:**
|
||||
- Header at line 40: `<div class="mb-8 flex items-center justify-between">` — **no max-w, no px, no mx-auto**
|
||||
- Content container at line 59: `<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full">`
|
||||
- The FleetView pattern at [`FleetView.vue`](frontend_app/src/views/vehicles/FleetView.vue:12) wraps EVERYTHING in `class="relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"` — including the header.
|
||||
|
||||
**Fix:** Wrap the local header in the same responsive container, OR add matching `mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full` classes to the header div. The cleanest approach: restructure so header + cards share the same container.
|
||||
|
||||
Apply the same fix to all 4 finance views (FinanceWalletsView, FinancePackagesView, FinanceTransactionsView).
|
||||
|
||||
### FAILURE 4: Backend Enum Error — 'credit' is not among defined enum values
|
||||
|
||||
**Root Cause:** The [`seed_financial_ledger.py`](backend/scripts/seed_financial_ledger.py:18) seed script inserts lowercase `"credit"` and `"debit"` strings into the PostgreSQL `audit.ledger_entry_type` enum column, but the enum definition at [`audit.py`](backend/app/models/system/audit.py:58-60) only allows `"CREDIT"` and `"DEBIT"` (UPPERCASE).
|
||||
|
||||
**Evidence:**
|
||||
- [`audit.py:58-60`](backend/app/models/system/audit.py:58-60):
|
||||
```python
|
||||
class LedgerEntryType(str, enum.Enum):
|
||||
DEBIT = "DEBIT"
|
||||
CREDIT = "CREDIT"
|
||||
```
|
||||
- [`seed_financial_ledger.py:18`](backend/scripts/seed_financial_ledger.py:18):
|
||||
```python
|
||||
DB_ENTRY_TYPES = ["credit", "debit"] # ❌ lowercase!
|
||||
```
|
||||
- The seed script uses `psycopg2` (sync) with explicit `::audit.ledger_entry_type` cast at line 105 — but psycopg2 may allow case-insensitive casting or the data was inserted before the enum was strict.
|
||||
- When the frontend calls `GET /billing/wallet/balance`, SQLAlchemy reads all `FinancialLedger` rows and tries to map `"credit"` to the enum — this crashes with the error seen in the screenshot.
|
||||
|
||||
**Fix (TWO parts):**
|
||||
|
||||
**Part A — Fix the seed script:**
|
||||
- [`seed_financial_ledger.py:18`](backend/scripts/seed_financial_ledger.py:18): Change `["credit", "debit"]` → `["CREDIT", "DEBIT"]`
|
||||
|
||||
**Part B — Fix existing bad data in the database:**
|
||||
Run SQL to normalize all lowercase entries:
|
||||
```sql
|
||||
UPDATE audit.financial_ledger SET entry_type = 'CREDIT' WHERE entry_type::text = 'credit';
|
||||
UPDATE audit.financial_ledger SET entry_type = 'DEBIT' WHERE entry_type::text = 'debit';
|
||||
```
|
||||
Also check and fix wallet_type and status similarly.
|
||||
|
||||
---
|
||||
|
||||
## STEP 2: CODE MODE HAND-OFF — Explicit Task List
|
||||
|
||||
Execute in order. Do NOT modify FleetView.vue, PrivateLayout.vue, BaseHeader.vue, HeaderBackButton.vue, or router/index.ts.
|
||||
|
||||
### Task 1: Remove HeaderBackButton from FinanceLayout.vue (FAILURE 2)
|
||||
|
||||
**File:** [`frontend_app/src/layouts/FinanceLayout.vue`](frontend_app/src/layouts/FinanceLayout.vue)
|
||||
|
||||
1. **DELETE lines 37-46** — the entire `HeaderBackButton` block and its comment:
|
||||
```html
|
||||
<!--
|
||||
Dynamic back button:
|
||||
- On /finance exactly → back to /dashboard
|
||||
- On any sub-route (/finance/wallets, etc.) → back to /finance
|
||||
-->
|
||||
<HeaderBackButton
|
||||
:to="backTarget"
|
||||
:label="t('common.back')"
|
||||
/>
|
||||
```
|
||||
|
||||
2. **DELETE the `backTarget` computed** (lines 90-100):
|
||||
```ts
|
||||
const backTarget = computed(() => {
|
||||
if (route.path === '/finance') {
|
||||
return '/dashboard'
|
||||
}
|
||||
return '/finance'
|
||||
})
|
||||
```
|
||||
|
||||
3. **Clean up imports:** Remove `import { computed } from 'vue'` (line 78) and `import { useRoute } from 'vue-router'` (line 79) if they're no longer used. Also remove `const route = useRoute()` (line 87). Remove `import HeaderBackButton from '../components/header/HeaderBackButton.vue'` (line 84).
|
||||
|
||||
### Task 2: Fix Header Alignment in FinanceMainView.vue (FAILURE 3)
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue)
|
||||
|
||||
1. **DELETE lines 39-55** (the standalone header div)
|
||||
|
||||
2. **INSERT the header INSIDE the content container**: Move it to be the first child of line 59's `<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full">`:
|
||||
```html
|
||||
<!-- ── Bottom-aligned card grid — matches DashboardView.vue exactly ── -->
|
||||
<div class="flex flex-col justify-end min-h-[85vh] pb-8">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full">
|
||||
<!-- ── Page header (FleetView.vue pattern) ── -->
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white">💰 {{ t('menu.finance') }}</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="router.push('/dashboard')"
|
||||
class="flex items-center gap-2 rounded-xl bg-white/10 px-4 py-2 text-sm text-white/80 transition-all duration-200 hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
{{ t('garage.backToDashboard') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 4-card grid — identical to DashboardView grid -->
|
||||
<div class="grid ...">
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Task 3: Fix Header Alignment in FinanceWalletsView.vue (FAILURE 3)
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceWalletsView.vue`](frontend_app/src/views/FinanceWalletsView.vue)
|
||||
|
||||
Same pattern as Task 2: Move the header inside the `mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full` container. The current structure:
|
||||
```
|
||||
<div class="mb-8 flex items-center justify-between"> ...header... </div>
|
||||
<div class="flex flex-col justify-start min-h-[85vh] py-8">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full">
|
||||
...content...
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Should become:
|
||||
```
|
||||
<div class="flex flex-col justify-start min-h-[85vh] py-8">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full">
|
||||
<div class="mb-8 flex items-center justify-between"> ...header... </div>
|
||||
...content...
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Task 4: Fix Header Alignment in FinancePackagesView.vue (FAILURE 3)
|
||||
|
||||
**File:** [`frontend_app/src/views/FinancePackagesView.vue`](frontend_app/src/views/FinancePackagesView.vue)
|
||||
|
||||
Same pattern as Task 3 — move header inside the container.
|
||||
|
||||
### Task 5: Fix Header Alignment in FinanceTransactionsView.vue (FAILURE 3)
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceTransactionsView.vue`](frontend_app/src/views/FinanceTransactionsView.vue)
|
||||
|
||||
Same pattern as Task 3 — move header inside the container.
|
||||
|
||||
### Task 6: Fix Backend Enum Error — Seed Script (FAILURE 4A)
|
||||
|
||||
**File:** [`backend/scripts/seed_financial_ledger.py`](backend/scripts/seed_financial_ledger.py)
|
||||
|
||||
1. **Line 18:** Change:
|
||||
```python
|
||||
DB_ENTRY_TYPES = ["credit", "debit"]
|
||||
```
|
||||
To:
|
||||
```python
|
||||
DB_ENTRY_TYPES = ["CREDIT", "DEBIT"]
|
||||
```
|
||||
|
||||
### Task 7: Fix Backend Enum Error — Existing Database Data (FAILURE 4B)
|
||||
|
||||
**Run these SQL commands** to normalize any existing lowercase entries:
|
||||
|
||||
```bash
|
||||
docker compose exec shared-postgres psql -U service_finder_app -d service_finder -c "
|
||||
UPDATE audit.financial_ledger SET entry_type = 'CREDIT' WHERE entry_type::text = 'credit';
|
||||
UPDATE audit.financial_ledger SET entry_type = 'DEBIT' WHERE entry_type::text = 'debit';
|
||||
-- Also check wallet_type
|
||||
SELECT DISTINCT entry_type::text, wallet_type::text, status::text FROM audit.financial_ledger;
|
||||
"
|
||||
```
|
||||
|
||||
### Task 8: Verify
|
||||
|
||||
1. **Frontend build:**
|
||||
```bash
|
||||
docker compose exec sf_public_frontend npm run build 2>&1 | tail -5
|
||||
```
|
||||
|
||||
2. **Backend wallet endpoint:**
|
||||
```bash
|
||||
docker compose exec sf_api python3 -c "
|
||||
import requests
|
||||
r = requests.post('http://localhost:8000/api/v1/auth/login', data={'username': 'admin@profibot.hu', 'password': 'Admin123!'})
|
||||
token = r.json()['access_token']
|
||||
resp = requests.get('http://localhost:8000/api/v1/billing/wallet/balance', headers={'Authorization': f'Bearer {token}'})
|
||||
print(f'Status: {resp.status_code}')
|
||||
print(resp.json())
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Summary Table
|
||||
|
||||
| # | Failure | Root Cause | File(s) to Fix |
|
||||
|---|---------|-----------|-----------------|
|
||||
| 1 | Mock data on dashboard | `GamificationCard` score is placeholder 0; `transactionCount` is placeholder 0 — by design from previous blueprint, pending gamification/transaction stores | None needed now (wiring to stores is future work) |
|
||||
| 2 | Back button still in top menu | [`FinanceLayout.vue`](frontend_app/src/layouts/FinanceLayout.vue:42-45) still has `<HeaderBackButton>` in `#left` slot | [`FinanceLayout.vue`](frontend_app/src/layouts/FinanceLayout.vue) |
|
||||
| 3 | Title misaligned to edge | Header is outside `mx-auto max-w-7xl px-4...` container | All 4 finance views |
|
||||
| 4 | Backend enum crash: `'credit' is not among defined enum values` | [`seed_financial_ledger.py:18`](backend/scripts/seed_financial_ledger.py:18) uses lowercase `"credit"/"debit"` but enum only accepts `"CREDIT"/"DEBIT"` | [`seed_financial_ledger.py`](backend/scripts/seed_financial_ledger.py) + DB cleanup SQL |
|
||||
|
||||
## ⚠️ DO NOT MODIFY
|
||||
|
||||
- [`FleetView.vue`](frontend_app/src/views/vehicles/FleetView.vue) — reference only
|
||||
- [`PrivateLayout.vue`](frontend_app/src/layouts/PrivateLayout.vue) — working
|
||||
- [`BaseHeader.vue`](frontend_app/src/components/layout/BaseHeader.vue) — working
|
||||
- [`HeaderBackButton.vue`](frontend_app/src/components/header/HeaderBackButton.vue) — working component, just needs to not be used in FinanceLayout
|
||||
- [`router/index.ts`](frontend_app/src/router/index.ts) — correct
|
||||
- [`DashboardView.vue`](frontend_app/src/views/DashboardView.vue) — working
|
||||
213
plans/finance_module_recovery_blueprint_2026-07-26.md
Normal file
213
plans/finance_module_recovery_blueprint_2026-07-26.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# 🔴 Finance Module Recovery Blueprint — Root Cause Analysis & Recovery Plan
|
||||
|
||||
**Date:** 2026-07-26
|
||||
**Author:** Architect Mode
|
||||
**Status:** Pending Code Mode Execution
|
||||
**Priority:** CRITICAL — Multiple systemic bugs across Frontend & Backend
|
||||
|
||||
---
|
||||
|
||||
## STEP 1: ARCHITECTURAL COMPARISON — Dashboard vs. Finance
|
||||
|
||||
### 1.1 Layout Wrapper Comparison
|
||||
|
||||
| Aspect | DashboardView (Working) | FinanceMainView (Broken) |
|
||||
|--------|-------------------------|--------------------------|
|
||||
| **Parent Layout** | PrivateLayout — provides BaseHeader with hamburger menu | FinanceLayout — provides same BaseHeader but with HeaderBackButton |
|
||||
| **Background** | Self-contained: `fixed inset-0` + `bg-[url('@/assets/garage-bg.png')]` in the component itself | Duplicated in layout: same `fixed inset-0` background in FinanceLayout |
|
||||
| **Header Teleport** | NO Teleport usage — DashboardView owns its own header via the background div | USES Teleport: FinanceMainView teleports `<span>` into `#header-teleport-target` |
|
||||
| **Router Nesting** | Dash is a child of PrivateLayout | Finance is a child of FinanceLayout |
|
||||
|
||||
### 1.2 Card Component Comparison
|
||||
|
||||
| Aspect | Working Dashboard Cards | Broken Finance Cards |
|
||||
|--------|------------------------|---------------------|
|
||||
| **Root Element** | Single `<div>` with CSS classes directly on the card (e.g., MyVehiclesCard.vue:2-4) | Same pattern: plain `<div>` with identical CSS ✅ |
|
||||
| **Click Handler** | Invisible overlay `<div>` with `@click` (e.g., MyVehiclesCard.vue:6-9) | Same pattern: invisible overlay with `@click` router push ✅ |
|
||||
| **Data Binding** | Uses Pinia stores (financeStore, vehicleStore, authStore) | Same Pinia stores used ✅ |
|
||||
| **Component Import** | Imports card components from dashboard/ | Standalone views — no card components imported, HTML inline |
|
||||
|
||||
### 1.3 Key Architectural Difference
|
||||
|
||||
**The only structural difference is the Teleport usage.** The working `DashboardView` uses `<Teleport to="body">` ONLY for its Flip Modal overlay, which is always available in the DOM. The broken `FinanceMainView` uses `<Teleport to="#header-teleport-target">` — a target that is conditionally rendered inside `BaseHeader`, which itself is nested inside `FinanceLayout`.
|
||||
|
||||
---
|
||||
|
||||
## STEP 2: ROOT CAUSE ANALYSIS OF 4 KNOWN BUGS
|
||||
|
||||
### 🔴 BUG 1: Teleport Freeze — `Failed to locate Teleport target with selector "#header-teleport-target"`
|
||||
|
||||
**Root Cause:** Vue Router lifecycle timing mismatch between Teleport mount/unmount and DOM availability.
|
||||
|
||||
**Evidence:**
|
||||
- `FinanceMainView.vue:38-40` uses `<Teleport to="#header-teleport-target">` to inject a page title
|
||||
- The target `#header-teleport-target` is defined in `BaseHeader.vue:15`
|
||||
- `BaseHeader` is rendered inside `FinanceLayout.vue:35`
|
||||
|
||||
**Failure scenario:**
|
||||
1. User navigates FROM `/finance` TO `/dashboard`
|
||||
2. Vue Router starts unmounting the `/finance` route tree: FinanceMainView → FinanceLayout → BaseHeader
|
||||
3. FinanceMainView tries to unmount its Teleported `<span>` from `#header-teleport-target`
|
||||
4. BUT BaseHeader (which contains the target) has already been destroyed or is in the process of being destroyed
|
||||
5. Vue throws: `Failed to locate Teleport target with selector "#header-teleport-target"`
|
||||
6. The app freezes because the Teleport cleanup throws an unhandled error that halts the transition
|
||||
|
||||
**The same issue can happen on MOUNT:** if FinanceMainView renders before BaseHeader finishes mounting, the Teleport target doesn't exist yet.
|
||||
|
||||
**Contrast with DashboardView:** The working `DashboardView:101` teleports to `"body"` — which **always** exists in the DOM, regardless of component lifecycle.
|
||||
|
||||
### 🔴 BUG 2: Component Resolution Error — `Failed to resolve component: TechDataTab`
|
||||
|
||||
**Root Cause:** Missing import statement in `VehicleDetailModal.vue`.
|
||||
|
||||
**Evidence:**
|
||||
- `VehicleDetailModal.vue:284` uses `<TechDataTab />` in its template
|
||||
- The `<script setup>` section (lines 892-903) imports: `VehiclePlateBadge`, `ProviderAutocomplete`, `ProviderQuickAddModal` — but **NOT** `TechDataTab`
|
||||
- `VehicleDetailsView.vue:112` correctly imports `TechDataTab from '../../components/vehicles/tabs/TechDataTab.vue'`
|
||||
- This is a copy-paste bug: the `<TechDataTab />` usage was copied from VehicleDetailsView but the import was not
|
||||
|
||||
**Failure scenario:**
|
||||
1. User opens VehicleDetailModal and clicks the "Műszaki adatok" tab
|
||||
2. Vue tries to render `<TechDataTab />` but cannot resolve the component
|
||||
3. Vue emits warning: `[Vue warn]: Failed to resolve component: TechDataTab`
|
||||
4. The tab shows blank/empty content
|
||||
|
||||
**Note:** This bug is NOT caused by the finance module changes — it's a pre-existing bug in VehicleDetailModal.vue.
|
||||
|
||||
### 🟡 BUG 3: Fragment/Emit Warning — `Extraneous non-emits event listeners (deleted)`
|
||||
|
||||
**Root Cause:** `DashboardView.vue:169` listens for `@deleted` but the current `VehicleFormModal.vue` does not declare or emit `deleted`.
|
||||
|
||||
**Evidence:**
|
||||
- `DashboardView.vue:164-170`: `<VehicleFormModal @deleted="onVehicleDeleted" />`
|
||||
- `VehicleFormModal.vue:1778-1782`:
|
||||
```ts
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
(e: 'saved'): void
|
||||
// NO "deleted" event declared!
|
||||
}>()
|
||||
```
|
||||
|
||||
**Failure scenario:**
|
||||
1. Vue 3 sees `@deleted` on the component but no matching emit declaration
|
||||
2. Vue tries to fall through and treat it as a native DOM event on the root element
|
||||
3. Since VehicleFormModal's root is `<Teleport>` (a fragment-like component), Vue cannot attach native DOM listeners
|
||||
4. Vue warns: `Extraneous non-emits event listeners (deleted) were passed to component`
|
||||
|
||||
### 🔴 BUG 4: Backend API Crash — HTTP 500 on `GET /api/v1/providers/categories`
|
||||
|
||||
**Root Cause:** The backend endpoint fails with an unhandled exception during ExpertiseTag query or ExpertiseCategoryOut serialization.
|
||||
|
||||
**Evidence:**
|
||||
- `providers.py:149-183`: The `/categories` endpoint queries ExpertiseTag and constructs ExpertiseCategoryOut objects
|
||||
- The endpoint wraps everything in try/except and returns HTTP 500 on any exception
|
||||
- Git status shows modified schema files — possible schema mismatch
|
||||
|
||||
**Required diagnostic:** Run `docker compose logs --tail 100 sf_api` after reproducing the 500 error.
|
||||
|
||||
---
|
||||
|
||||
## STEP 3: THE RECOVERY BLUEPRINT
|
||||
|
||||
### Phase 1: Stabilize the Frontend
|
||||
|
||||
#### Fix 1.1: Teleport Target Lifecycle (BUG 1)
|
||||
- **File:** `frontend_app/src/views/FinanceMainView.vue`
|
||||
- **Solution:** Wrap Teleport in a v-if guard tied to onMounted/onBeforeUnmount lifecycle:
|
||||
```html
|
||||
<Teleport v-if="teleportReady" to="#header-teleport-target">
|
||||
```
|
||||
- **Also apply to:** FinanceWalletsView, FinancePackagesView, FinanceTransactionsView
|
||||
|
||||
#### Fix 1.2: Missing TechDataTab Import (BUG 2)
|
||||
- **File:** `frontend_app/src/components/vehicle/VehicleDetailModal.vue`
|
||||
- **Action:** Add `import TechDataTab from '../vehicles/tabs/TechDataTab.vue'` after line 903
|
||||
|
||||
#### Fix 1.3: Fragment Emit Warning (BUG 3)
|
||||
- **File:** `frontend_app/src/components/dashboard/VehicleFormModal.vue`
|
||||
- **Action:** Add `(e: 'deleted'): void` to defineEmits and emit on deletion
|
||||
|
||||
### Phase 2: Diagnose & Fix the Backend
|
||||
|
||||
#### Fix 2.1: Backend 500 on providers/categories (BUG 4)
|
||||
- Restart sf_api container and capture live logs
|
||||
- Trigger the endpoint and analyze the Python traceback
|
||||
- Fix based on actual error (model/schema/DB issue)
|
||||
|
||||
---
|
||||
|
||||
## STEP 4: CODE MODE HAND-OFF INSTRUCTIONS
|
||||
|
||||
### Task 1: Fix Teleport Lifecycle (BUG 1) — CRITICAL
|
||||
|
||||
1. Open: `frontend_app/src/views/FinanceMainView.vue`
|
||||
2. Add `ref` and `onBeforeUnmount` to vue imports (line 237)
|
||||
3. Add: `const teleportReady = ref(false)`
|
||||
4. In onMounted, add: `teleportReady.value = true`
|
||||
5. Add: `onBeforeUnmount(() => { teleportReady.value = false })`
|
||||
6. Change line 38: `<Teleport v-if="teleportReady" to="#header-teleport-target">`
|
||||
7. Apply same pattern to: `FinanceWalletsView.vue`, `FinancePackagesView.vue`, `FinanceTransactionsView.vue`
|
||||
|
||||
### Task 2: Fix Missing TechDataTab Import (BUG 2) — HIGH
|
||||
|
||||
1. Open: `frontend_app/src/components/vehicle/VehicleDetailModal.vue`
|
||||
2. After line 903, add:
|
||||
```ts
|
||||
import TechDataTab from '../vehicles/tabs/TechDataTab.vue'
|
||||
```
|
||||
|
||||
### Task 3: Fix Fragment Emit Warning (BUG 3) — MEDIUM
|
||||
|
||||
1. Open: `frontend_app/src/components/dashboard/VehicleFormModal.vue`
|
||||
2. At line 1778-1782, add `(e: 'deleted'): void` to defineEmits
|
||||
3. Find delete/archive handler, add `emit('deleted')` after successful operation
|
||||
|
||||
### Task 4: Diagnose Backend 500 (BUG 4) — HIGH
|
||||
|
||||
1. Run: `docker compose restart sf_api`
|
||||
2. Run diagnostic query:
|
||||
```bash
|
||||
docker compose exec sf_api python3 -c "
|
||||
import asyncio
|
||||
from app.database import async_session
|
||||
from sqlalchemy import select, text
|
||||
from app.models.data.expertise_tag import ExpertiseTag
|
||||
async def main():
|
||||
async with async_session() as db:
|
||||
stmt = select(ExpertiseTag).order_by(ExpertiseTag.level, ExpertiseTag.id).limit(10)
|
||||
result = await db.execute(stmt)
|
||||
tags = result.scalars().all()
|
||||
print(f'Query OK: {len(tags)} tags')
|
||||
asyncio.run(main())
|
||||
"
|
||||
```
|
||||
3. Based on output, fix providers.py or the ExpertiseTag model
|
||||
|
||||
### Task 5: Verify All Fixes — MANDATORY
|
||||
|
||||
1. Frontend build: `cd frontend_app && npm run build 2>&1 | tail -20`
|
||||
2. Backend imports: `docker compose exec sf_api python3 -c "from app.main import app; print('OK')"`
|
||||
3. Navigate `/finance` → all 4 cards → `/dashboard` — verify no freeze
|
||||
4. VehicleDetailModal → "Műszaki adatok" tab — verify renders
|
||||
|
||||
---
|
||||
|
||||
## 📋 Summary Table
|
||||
|
||||
| # | Bug | Severity | Root Cause | File(s) to Fix |
|
||||
|---|-----|----------|------------|-----------------|
|
||||
| 1 | Teleport Freeze | CRITICAL | Teleport target lifecycle mismatch | FinanceMainView.vue:38 + 3 sub-views |
|
||||
| 2 | TechDataTab not found | HIGH | Missing import | VehicleDetailModal.vue:284 |
|
||||
| 3 | Fragment emit warning | MEDIUM | Missing emit declaration | VehicleFormModal.vue:1778 |
|
||||
| 4 | Backend 500 error | HIGH | Unhandled exception in /providers/categories | providers.py:149-183 (needs live diagnosis) |
|
||||
|
||||
## ⚠️ DO NOT MODIFY
|
||||
|
||||
The following files are working correctly and MUST NOT be changed:
|
||||
- `DashboardView.vue` — working dashboard
|
||||
- `PrivateLayout.vue` — working layout
|
||||
- `BaseHeader.vue` — working header
|
||||
- `FinancialCard.vue` — working card
|
||||
- `MyVehiclesCard.vue` — working card
|
||||
- `router/index.ts` — correct route configuration
|
||||
219
plans/finance_navbar_and_alignment_fix_2026-07-26.md
Normal file
219
plans/finance_navbar_and_alignment_fix_2026-07-26.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# 🔵 Finance Module — Navbar Title & Vertical Alignment Fix Blueprint
|
||||
|
||||
**Date:** 2026-07-26
|
||||
**Author:** Architect Mode
|
||||
**Status:** Pending Code Mode Execution
|
||||
**Priority:** MEDIUM — UX polish
|
||||
|
||||
---
|
||||
|
||||
## STEP 1: LOGO NAVIGATION ANALYSIS
|
||||
|
||||
**File:** [`HeaderLogo.vue`](frontend_app/src/components/header/HeaderLogo.vue)
|
||||
|
||||
**Finding:** The logo ALREADY navigates to `/dashboard` correctly (line 37). The [`targetRoute`](frontend_app/src/components/header/HeaderLogo.vue:32-38) computed property checks if the user is on an `/organization/` page (routes to org root), otherwise routes to `/dashboard`.
|
||||
|
||||
**Conclusion:** Logo navigation is already correct. If it doesn't work on the Garage page, the issue is likely a z-index/CSS `pointer-events` conflict with the dark overlay in FleetView. But the `router-link` itself is properly configured.
|
||||
|
||||
**No code changes needed for the logo.**
|
||||
|
||||
---
|
||||
|
||||
## STEP 2: TOP NAVBAR TITLE RESTORATION
|
||||
|
||||
**The Problem:** Previous blueprints removed ALL `<Teleport to="#header-teleport-target">` usage from finance views. This removed BOTH the back button AND the page title. We need the title back in the navbar center, but WITHOUT the back button.
|
||||
|
||||
**The Solution (Cleanest approach):** Inject the title directly from [`FinanceLayout.vue`](frontend_app/src/layouts/FinanceLayout.vue) using the `#center` slot of `BaseHeader`. The layout component already knows which route is active via `useRoute()`. It can compute a dynamic title for the center slot.
|
||||
|
||||
**Current FinanceLayout `#center` slot:**
|
||||
```html
|
||||
<template #center>
|
||||
<!-- Teleport target for child route page titles -->
|
||||
</template>
|
||||
```
|
||||
|
||||
**Fix:** Replace with:
|
||||
```html
|
||||
<template #center>
|
||||
<span class="text-white font-bold text-sm tracking-wide">{{ financeTitle }}</span>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Add computed in script:**
|
||||
```ts
|
||||
const financeTitle = computed(() => {
|
||||
if (route.path === '/finance') return '💰 ' + t('menu.finance')
|
||||
if (route.path.startsWith('/finance/wallets')) return '💰 ' + t('finance.wallet')
|
||||
if (route.path.startsWith('/finance/packages')) return '📦 ' + t('subscription.title')
|
||||
if (route.path.startsWith('/finance/transactions')) return '📋 ' + t('finance.transactionHistory')
|
||||
return '💰 ' + t('menu.finance')
|
||||
})
|
||||
```
|
||||
|
||||
This puts the title in the navbar center WITHOUT any Teleport from child views. No Teleport lifecycle issues. No back button duplication. The title appears in the exact same spot as the original Teleported title did.
|
||||
|
||||
**Also:** Remove the duplicate `<h1>` titles from all 4 finance views' local content headers since the title is now in the navbar. Keep the back button in the local header (right-aligned).
|
||||
|
||||
---
|
||||
|
||||
## STEP 3: VERTICAL ALIGNMENT FIX — Push Cards to Bottom
|
||||
|
||||
**The Problem:** The FleetView clone used `py-8` (top-aligned), but the user wants cards at the bottom of the screen (like the Dashboard).
|
||||
|
||||
**Dashboard pattern (from [`DashboardView.vue`](frontend_app/src/views/DashboardView.vue:39)):**
|
||||
```html
|
||||
<div class="flex flex-col justify-end min-h-[85vh] pb-8">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full">
|
||||
<!-- cards -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Current FinanceMainView (after FleetView clone):**
|
||||
```html
|
||||
<div class="relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<!-- header + cards -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**Fix — Hybrid approach (FleetView container + Dashboard bottom-alignment):**
|
||||
|
||||
Each finance view's outer structure should become:
|
||||
```html
|
||||
<div class="relative min-h-screen">
|
||||
<div class="relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8 flex flex-col justify-end min-h-[85vh] pb-8">
|
||||
<!-- back button row (right-aligned, no title) -->
|
||||
<div class="mb-8 flex items-center justify-end">
|
||||
<button @click="router.push('/dashboard')" ...>{{ t('garage.backToDashboard') }}</button>
|
||||
</div>
|
||||
<!-- cards grid -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Outer `relative min-h-screen` stays (for z-index context)
|
||||
- Inner container gets BOTH `mx-auto max-w-7xl...` (responsive width) AND `flex flex-col justify-end min-h-[85vh] pb-8` (bottom alignment)
|
||||
- Header simplified to just a right-aligned back button (no title — title is in navbar)
|
||||
- Same pattern applied to all 4 finance views
|
||||
|
||||
---
|
||||
|
||||
## STEP 4: CODE MODE HAND-OFF — Explicit Task List
|
||||
|
||||
### Task 1: Add dynamic title to FinanceLayout navbar center
|
||||
|
||||
**File:** [`frontend_app/src/layouts/FinanceLayout.vue`](frontend_app/src/layouts/FinanceLayout.vue)
|
||||
|
||||
1. **Replace the `#center` slot** (currently empty with just a comment):
|
||||
```html
|
||||
<template #center>
|
||||
<span class="text-white font-bold text-sm tracking-wide">{{ financeTitle }}</span>
|
||||
</template>
|
||||
```
|
||||
|
||||
2. **Add `computed` import** (if not present — it was removed in previous cleanup, so add back):
|
||||
```ts
|
||||
import { computed } from 'vue'
|
||||
```
|
||||
Add to existing `vue` import line alongside `useI18n`.
|
||||
|
||||
3. **Add `useRoute` import:**
|
||||
```ts
|
||||
import { useRoute } from 'vue-router'
|
||||
```
|
||||
(Was removed in previous cleanup — add back)
|
||||
|
||||
4. **Add route and computed after `const { t } = useI18n()`:**
|
||||
```ts
|
||||
const route = useRoute()
|
||||
|
||||
const financeTitle = computed(() => {
|
||||
if (route.path === '/finance') return '💰 ' + t('menu.finance')
|
||||
if (route.path.startsWith('/finance/wallets')) return '💰 ' + t('finance.wallet')
|
||||
if (route.path.startsWith('/finance/packages')) return '📦 ' + t('subscription.title')
|
||||
if (route.path.startsWith('/finance/transactions')) return '📋 ' + t('finance.transactionHistory')
|
||||
return '💰 ' + t('menu.finance')
|
||||
})
|
||||
```
|
||||
|
||||
### Task 2: Fix vertical alignment + simplify header in FinanceMainView
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue)
|
||||
|
||||
**Change the outer container structure (lines 39-40):**
|
||||
|
||||
FROM:
|
||||
```html
|
||||
<div class="relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
```
|
||||
|
||||
TO:
|
||||
```html
|
||||
<div class="relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8 flex flex-col justify-end min-h-[85vh] pb-8">
|
||||
```
|
||||
|
||||
**Simplify the header (lines 41-57):** Remove the left-side `<h1>` title (it's now in the navbar). Keep only the right-aligned back button:
|
||||
|
||||
```html
|
||||
<!-- ── Back button (title in top navbar) ── -->
|
||||
<div class="mb-8 flex items-center justify-end">
|
||||
<button
|
||||
@click="router.push('/dashboard')"
|
||||
class="flex items-center gap-2 rounded-xl bg-white/10 px-4 py-2 text-sm text-white/80 transition-all duration-200 hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
{{ t('garage.backToDashboard') }}
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Task 3: Fix vertical alignment + simplify header in FinanceWalletsView
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceWalletsView.vue`](frontend_app/src/views/FinanceWalletsView.vue)
|
||||
|
||||
1. Change outer container to `relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8 flex flex-col justify-end min-h-[85vh] pb-8`
|
||||
2. Simplify header to right-aligned back button only (same pattern as Task 2), pointing to `/finance`
|
||||
|
||||
### Task 4: Fix vertical alignment + simplify header in FinancePackagesView
|
||||
|
||||
**File:** [`frontend_app/src/views/FinancePackagesView.vue`](frontend_app/src/views/FinancePackagesView.vue)
|
||||
|
||||
Same as Task 3 — right-aligned back button to `/finance`, no title.
|
||||
|
||||
### Task 5: Fix vertical alignment + simplify header in FinanceTransactionsView
|
||||
|
||||
**File:** [`frontend_app/src/views/FinanceTransactionsView.vue`](frontend_app/src/views/FinanceTransactionsView.vue)
|
||||
|
||||
Same as Task 3 — right-aligned back button to `/finance`, no title.
|
||||
|
||||
### Task 6: Build verification
|
||||
|
||||
```bash
|
||||
docker compose exec sf_public_frontend npm run build 2>&1 | tail -5
|
||||
```
|
||||
|
||||
Must show `✓ built in X.XXs` with no errors.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Summary
|
||||
|
||||
| # | Fix | File(s) |
|
||||
|---|-----|---------|
|
||||
| 1 | Title in top navbar via FinanceLayout center slot | [`FinanceLayout.vue`](frontend_app/src/layouts/FinanceLayout.vue) |
|
||||
| 2 | Bottom-aligned cards + simplified header | [`FinanceMainView.vue`](frontend_app/src/views/FinanceMainView.vue) |
|
||||
| 3 | Bottom-aligned cards + simplified header | [`FinanceWalletsView.vue`](frontend_app/src/views/FinanceWalletsView.vue) |
|
||||
| 4 | Bottom-aligned cards + simplified header | [`FinancePackagesView.vue`](frontend_app/src/views/FinancePackagesView.vue) |
|
||||
| 5 | Bottom-aligned cards + simplified header | [`FinanceTransactionsView.vue`](frontend_app/src/views/FinanceTransactionsView.vue) |
|
||||
| 6 | Build verification | `npm run build` |
|
||||
|
||||
## ⚠️ DO NOT MODIFY
|
||||
|
||||
- [`HeaderLogo.vue`](frontend_app/src/components/header/HeaderLogo.vue) — already correct
|
||||
- [`BaseHeader.vue`](frontend_app/src/components/layout/BaseHeader.vue) — working
|
||||
- [`FleetView.vue`](frontend_app/src/views/vehicles/FleetView.vue) — reference only
|
||||
- [`PrivateLayout.vue`](frontend_app/src/layouts/PrivateLayout.vue) — working
|
||||
- [`router/index.ts`](frontend_app/src/router/index.ts) — correct
|
||||
156
plans/logic_spec_fix_401_ledger_endpoint.md
Normal file
156
plans/logic_spec_fix_401_ledger_endpoint.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Logic Spec: Fix 401 Unauthorized on Admin Ledger Endpoint
|
||||
|
||||
**Date:** 2026-07-25
|
||||
**Severity:** P0 (Blocker — admin users cannot access Financial Ledger page)
|
||||
**Related Issue:** Frontend redirects to login screen on `/finance/ledger`
|
||||
|
||||
---
|
||||
|
||||
## 1. Root Cause Analysis
|
||||
|
||||
### 1.1 Authentication Architecture
|
||||
|
||||
The FastAPI backend uses `OAuth2PasswordBearer` ([`deps.py:27-29`](backend/app/api/deps.py:27)) which extracts the JWT token **exclusively** from the `Authorization: Bearer <token>` HTTP header. It does **NOT** read tokens from cookies.
|
||||
|
||||
```python
|
||||
# backend/app/api/deps.py:27-29
|
||||
reusable_oauth2 = OAuth2PasswordBearer(
|
||||
tokenUrl=f"{settings.API_V1_STR}/auth/login"
|
||||
)
|
||||
```
|
||||
|
||||
The auth dependency chain for the ledger endpoint:
|
||||
|
||||
```
|
||||
GET /admin/finance/ledger
|
||||
→ RequirePermission("finance:view") [deps.py:283]
|
||||
→ get_current_active_user [deps.py:94]
|
||||
→ get_current_user [deps.py:56]
|
||||
→ get_current_token_payload [deps.py:31]
|
||||
→ reusable_oauth2 (OAuth2PasswordBearer) [deps.py:27]
|
||||
→ extracts token from Authorization: Bearer header
|
||||
```
|
||||
|
||||
### 1.2 The Bug
|
||||
|
||||
The [`ledger.vue:348`](frontend_admin/pages/finance/ledger.vue:348) uses **native `fetch()`**:
|
||||
|
||||
```typescript
|
||||
const res = await fetch(`/api/v1/admin/finance/ledger?${params.toString()}`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
```
|
||||
|
||||
This sends cookies (including `access_token` cookie) but does **NOT** send the `Authorization: Bearer` header. The `OAuth2PasswordBearer` dependency never sees a token → 401 Unauthorized.
|
||||
|
||||
### 1.3 Why Other Admin Pages Work
|
||||
|
||||
Every other admin page uses `$fetch()` (Nuxt's ofetch wrapper) with a `getHeaders()` helper that explicitly adds the `Authorization` header:
|
||||
|
||||
```typescript
|
||||
function getHeaders(): Record<string, string> {
|
||||
const tokenCookie = useCookie('access_token')
|
||||
return tokenCookie.value
|
||||
? { Authorization: `Bearer ${tokenCookie.value}` }
|
||||
: {}
|
||||
}
|
||||
```
|
||||
|
||||
Examples:
|
||||
- [`commission-rules.vue:694`](frontend_admin/pages/finance/commission-rules.vue:694) — has `getHeaders()`
|
||||
- [`providers/index.vue:288`](frontend_admin/pages/providers/index.vue:288) — has `getHeaders()`
|
||||
- [`gamification/ledger.vue:244`](frontend_admin/pages/gamification/ledger.vue:244) — has `getHeaders()`
|
||||
|
||||
The `ledger.vue` page **omitted** both `$fetch` and the `getHeaders()` helper — it's the only admin page with this pattern.
|
||||
|
||||
### 1.4 What the 401 Interceptor Does
|
||||
|
||||
The [`api.ts` plugin](frontend_admin/plugins/api.ts:5) overrides `globalThis.fetch` to detect 401 responses. When a 401 is detected on any non-login URL, it calls `authStore.logout()` and redirects to `/login`. This is why the user sees a redirect — the interceptor fires, clears the token, and forces a redirect.
|
||||
|
||||
**The interceptor is working correctly.** The problem is the request never had a token attached in the first place.
|
||||
|
||||
---
|
||||
|
||||
## 2. Fix Plan
|
||||
|
||||
### 2.1 Primary Fix: Frontend — [`ledger.vue`](frontend_admin/pages/finance/ledger.vue)
|
||||
|
||||
**File to modify:** `frontend_admin/pages/finance/ledger.vue`
|
||||
|
||||
**Changes:**
|
||||
|
||||
1. Add `getHeaders()` helper function in the `<script setup>` block:
|
||||
|
||||
```typescript
|
||||
function getHeaders(): Record<string, string> {
|
||||
const tokenCookie = useCookie('access_token')
|
||||
return tokenCookie.value
|
||||
? { Authorization: `Bearer ${tokenCookie.value}` }
|
||||
: {}
|
||||
}
|
||||
```
|
||||
|
||||
2. Add `useCookie` import at the top:
|
||||
|
||||
```typescript
|
||||
import { useCookie } from '#app'
|
||||
```
|
||||
|
||||
3. Replace the native `fetch()` call with `$fetch()` and pass headers:
|
||||
|
||||
```typescript
|
||||
// BEFORE (broken):
|
||||
const res = await fetch(`/api/v1/admin/finance/ledger?${params.toString()}`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
// AFTER (fixed):
|
||||
const data: LedgerResponse = await $fetch('/api/v1/admin/finance/ledger', {
|
||||
headers: getHeaders(),
|
||||
params: params, // $fetch handles query params natively
|
||||
})
|
||||
```
|
||||
|
||||
4. Adjust error handling — `$fetch` throws on non-2xx responses.
|
||||
|
||||
### 2.2 No Backend Changes Required
|
||||
|
||||
The [`finance_admin.py`](backend/app/api/v1/endpoints/finance_admin.py:81-96) security dependencies are **correct**:
|
||||
|
||||
```python
|
||||
@router.get("/ledger", response_model=FinancialLedgerListResponse)
|
||||
async def list_financial_ledger(
|
||||
...
|
||||
current_user: User = Depends(deps.get_current_active_user),
|
||||
_ = Depends(deps.RequirePermission("finance:view")),
|
||||
):
|
||||
```
|
||||
|
||||
This is consistent with working endpoints like:
|
||||
- [`admin_commission.py:72`](backend/app/api/v1/endpoints/admin_commission.py:72): `current_user: User = Depends(get_current_staff)`
|
||||
- [`finance_admin.py:30-31`](backend/app/api/v1/endpoints/finance_admin.py:30): `list_issuers` uses identical pattern and works
|
||||
|
||||
The 401 is purely a **frontend auth header omission** — the backend auth chain and permission check are correct.
|
||||
|
||||
---
|
||||
|
||||
## 3. Verification Steps
|
||||
|
||||
1. After applying the fix, the frontend dev server should be running.
|
||||
2. Log in as admin (`admin@profibot.hu` / `Admin123!`).
|
||||
3. Navigate to `/finance/ledger`.
|
||||
4. Verify:
|
||||
- No 401 error in browser DevTools Network tab
|
||||
- `Authorization: Bearer <token>` header is present in the request
|
||||
- Ledger data loads with pagination, filtering
|
||||
- No redirect to `/login`
|
||||
|
||||
---
|
||||
|
||||
## 4. Impact Assessment
|
||||
|
||||
| Component | Change | Risk |
|
||||
|-----------|--------|------|
|
||||
| `ledger.vue` | Add `getHeaders()`, `useCookie` import, switch to `$fetch` | Low — exact same pattern used by 20+ other admin pages |
|
||||
| Backend | None | N/A |
|
||||
| Other pages | None | N/A |
|
||||
Reference in New Issue
Block a user