# P0 Audit: Private Dashboard (`/dashboard`) UI & State Architecture **Date:** 2026-06-21 **Scope:** Read-only audit of routing, components, store, and navigation paths **Auditor:** Fast Coder (Core Developer) --- ## 1. Routing Architecture **Router file:** [`frontend/src/router/index.ts`](frontend/src/router/index.ts) The `/dashboard` route is a **child route** under the [`PrivateLayout.vue`](frontend/src/layouts/PrivateLayout.vue) wrapper: | Path | Name | Component | Auth | |------|------|-----------|------| | `/dashboard` | `dashboard` | [`DashboardView.vue`](frontend/src/views/DashboardView.vue) | `requiresAuth` | | `/dashboard/vehicles` | `FleetView` | [`FleetView.vue`](frontend/src/views/vehicles/FleetView.vue) | `requiresAuth` | | `/dashboard/vehicles/:id` | `VehicleDetails` | [`VehicleDetailsView.vue`](frontend/src/views/vehicles/VehicleDetailsView.vue) | `requiresAuth` | | `/dashboard/service-finder` | `service-finder` | [`ServiceFinderView.vue`](frontend/src/views/ServiceFinderView.vue) | `requiresAuth` | | `/dashboard/subscription` | `subscription` | [`SubscriptionPlansView.vue`](frontend/src/views/SubscriptionPlansView.vue) | `requiresAuth` | **Corporate mirror routes** exist under `/organization/:id/` with identical child structure. --- ## 2. DashboardView.vue — Main Entry Point **File:** [`frontend/src/views/DashboardView.vue`](frontend/src/views/DashboardView.vue) ### Data Fetching (Lifecycle) ```typescript onMounted(() => { vehicleStore.fetchVehicles() // GET /assets/vehicles authStore.fetchMyOrganizations() // GET /organizations/my }) ``` - **`fetchVehicles()`** is called unconditionally on mount. - **`fetchMyOrganizations()`** loads the user's org list for corporate mode detection. - No cost/financial data is fetched at the dashboard level — costs are lazy-loaded per-vehicle in the detail modal. ### 5-Card Grid Layout The dashboard renders 5 cards in a `grid-cols-1 md:grid-cols-3 lg:grid-cols-5` layout: | # | Card Component | File | Events | |---|---------------|------|--------| | 1 | `MyVehiclesCard` | [`frontend/src/components/dashboard/MyVehiclesCard.vue`](frontend/src/components/dashboard/MyVehiclesCard.vue) | `@open-card` | | 2 | `CostsActionsCard` | [`frontend/src/components/dashboard/CostsActionsCard.vue`](frontend/src/components/dashboard/CostsActionsCard.vue) | `@cost-saved` | | 3 | `ServiceFinderCard` | [`frontend/src/components/dashboard/ServiceFinderCard.vue`](frontend/src/components/dashboard/ServiceFinderCard.vue) | — | | 4 | `GamificationCard` | [`frontend/src/components/dashboard/GamificationCard.vue`](frontend/src/components/dashboard/GamificationCard.vue) | `@open-card` | | 5 | `ProfileTrustCard` | [`frontend/src/components/dashboard/ProfileTrustCard.vue`](frontend/src/components/dashboard/ProfileTrustCard.vue) | `@open-card` | ### Modal System The DashboardView manages **3 modal layers**: 1. **Flip Modal** (`activeCard`): A 3D-flip overlay that shows `PrivateVehicleManager` when `activeCard === 'vehicles'`. 2. **VehicleDetailModal**: Deep-link modal with 4 tabs (Basics, Finance, Alerts, ServiceBook). 3. **VehicleFormModal**: Add/Edit vehicle form (standalone, decoupled from the manager). --- ## 3. MyVehiclesCard — The Garage Tile **File:** [`frontend/src/components/dashboard/MyVehiclesCard.vue`](frontend/src/components/dashboard/MyVehiclesCard.vue) ### Data Source ```typescript const displayVehicles = computed(() => vehicleStore.sortedVehicles) ``` Uses the Pinia store's `sortedVehicles` getter directly. Shows max **3 vehicles** (`.slice(0, 3)`). ### Vehicle Rendering Each vehicle is rendered via [`VehicleCardCompact`](frontend/src/components/vehicle/VehicleCardCompact.vue) — a minimal row component showing: - `license_plate` (via `VehiclePlateBadge`) - `brand` / `model` / `nickname` ### Click Actions - **Card click** → `navigateToFleet()` → `/dashboard/vehicles` (or `/organization/:id/vehicles`) - **Plate click** → `openVehicleDetail(vehicle)` → `/dashboard/vehicles/:id` (or `/organization/:id/vehicles/:id`) --- ## 4. VehicleCardStandard — Full Tile Component **File:** [`frontend/src/components/vehicle/VehicleCardStandard.vue`](frontend/src/components/vehicle/VehicleCardStandard.vue) ### Props Used (template bindings) | Template Expression | Backend `AssetResponse` Field | Status | |---|---|---| | `vehicle.image` | ❌ **NOT in AssetResponse** | ⚠️ **BROKEN** — `AssetResponse` has no `image` field | | `vehicle.brand` | ✅ `brand` | OK | | `vehicle.nickname` | ❌ **NOT in AssetResponse** | ⚠️ **BROKEN** — No `nickname` field in schema | | `vehicle.license_plate` | ✅ `license_plate` | OK | | `vehicle.countryCode` / `vehicle.country_code` | ❌ **NOT in AssetResponse** | ⚠️ **BROKEN** — No country code field | | `vehicle.model` | ✅ `model` | OK | | `vehicle.year_of_manufacture` / `vehicle.year` | ✅ `year_of_manufacture` | OK | | `vehicle.current_mileage` / `vehicle.mileage` | ✅ `current_mileage` | OK | | `vehicle.engine` / `vehicle.fuel_type` | ❌ `engine` **NOT in AssetResponse** | ⚠️ **BROKEN** — `engine` is a display-only field; `fuel_type` is OK | | `vehicle.individual_equipment?.color` / `vehicle.color` | ❌ `color` **NOT in AssetResponse** | ⚠️ **BROKEN** — `color` is stored in `individual_equipment` JSONB, but the root `color` field doesn't exist | | `vehicle.nextService` | ❌ **NOT in AssetResponse** | ⚠️ **BROKEN** — No `nextService` field | ### Critical Broken Bindings Summary | Field | Used In | Backend Reality | |-------|---------|-----------------| | `vehicle.image` | CardStandard (image area) | Not in `AssetResponse` — always shows placeholder | | `vehicle.nickname` | CardStandard, CardCompact | Not in `AssetResponse` — always null | | `vehicle.countryCode` / `country_code` | CardStandard, CardCompact, DetailModal | Not in `AssetResponse` — plate badge shows no country | | `vehicle.engine` | CardStandard (engine row) | Not in `AssetResponse` — falls back to `fuel_type` | | `vehicle.color` | CardStandard (color row) | Not in `AssetResponse` — stored in `individual_equipment.color` | | `vehicle.nextService` | CardStandard (service row) | Not in `AssetResponse` — always shows "—" | | `vehicle.mileage` (deprecated) | CardStandard, DetailModal | Falls back to `current_mileage` — OK but deprecated | --- ## 5. Pinia Store Audit **File:** [`frontend/src/stores/vehicle.ts`](frontend/src/stores/vehicle.ts) ### API Endpoint ```typescript const res = await api.get('/assets/vehicles') ``` ✅ **Correct endpoint** — matches backend `GET /api/v1/assets/vehicles`. ### Vehicle Interface vs Backend Schema The frontend `Vehicle` interface (line 10-109) is **mostly aligned** with the backend [`AssetResponse`](backend/app/schemas/asset.py:32). However: | Frontend `Vehicle` Field | Backend `AssetResponse` | Match | |---|---|---| | `id: string` | `id: UUID` | ✅ | | `vin` | `vin` | ✅ | | `license_plate` | `license_plate` | ✅ | | `name` | `name` | ✅ | | `catalog_id` | `catalog_id` | ✅ | | `vehicle_class` | `vehicle_class` | ✅ | | `brand` | `brand` | ✅ | | `model` | `model` | ✅ | | `trim_level` | `trim_level` | ✅ | | `fuel_type` | `fuel_type` | ✅ | | `engine_capacity` | `engine_capacity` | ✅ | | `power_kw` | `power_kw` | ✅ | | `torque_nm` | `torque_nm` | ✅ | | `cylinder_layout` | `cylinder_layout` | ✅ | | `transmission_type` | `transmission_type` | ✅ | | `drive_type` | `drive_type` | ✅ | | `euro_classification` | `euro_classification` | ✅ | | `curb_weight` | `curb_weight` | ✅ | | `max_weight` | `max_weight` | ✅ | | `door_count` | `door_count` | ✅ | | `seat_count` | `seat_count` | ✅ | | `current_mileage` | `current_mileage` | ✅ | | `condition_score` | `condition_score` | ✅ | | `status` | `status` | ✅ | | `is_verified` | `is_verified` | ✅ | | `year_of_manufacture` | `year_of_manufacture` | ✅ | | `created_at` | `created_at` | ✅ | | `updated_at` | `updated_at` | ✅ | | `is_under_warranty` | `is_under_warranty` | ✅ | | `warranty_expiry_date` | `warranty_expiry_date` | ✅ | | `registration_certificate_number` | `registration_certificate_number` | ✅ | | `registration_certificate_validity` | `registration_certificate_validity` | ✅ | | `vehicle_registration_document_number` | `vehicle_registration_document_number` | ✅ | | `registration_country` | `registration_country` | ✅ | | `first_domestic_registration_date` | `first_domestic_registration_date` | ✅ | | `import_country` | `import_country` | ✅ | | `title_document_number` | `title_document_number` | ✅ | | `engine_number` | `engine_number` | ✅ | | `number_of_previous_owners` | `number_of_previous_owners` | ✅ | | `is_for_sale` | `is_for_sale` | ✅ | | `price` | `price` | ✅ | | `currency` | `currency` | ✅ | | `is_primary` | `is_primary` | ✅ | | `current_organization_id` | `current_organization_id` | ✅ | | `owner_organization_id` | `owner_organization_id` | ✅ | | `individual_equipment` | `individual_equipment` | ✅ | **The store interface is well-aligned with the backend.** The broken bindings are in the **display components** (`VehicleCardStandard`, `VehicleCardCompact`) which reference fields that exist in the `VehicleData` type (a superset with mock-data fallbacks) but not in the actual backend response. --- ## 6. Navigation / Drill-Down Paths ### Path 1: Dashboard → Fleet View ``` /dashboard → click card → /dashboard/vehicles (FleetView.vue) ``` FleetView renders all vehicles in a `BaseCard` grid. Clicking a card navigates to: ``` /dashboard/vehicles/:id (VehicleDetailsView.vue) ``` ### Path 2: Dashboard → Vehicle Detail Modal (in-page) ``` /dashboard → plate-click → VehicleDetailModal (Teleport) ``` This is an **in-page modal**, not a route change. The modal has 4 tabs: - **Alapadatok** (Basics) — reads from `AssetResponse` directly - **Pénzügyek (TCO)** — lazy-fetches `GET /assets/{id}/costs` - **Figyelmeztetések** — reads from `individual_equipment` JSONB - **Szervizkönyv** — lazy-fetches `GET /assets/{id}/events` ### Path 3: Dashboard → Flip Modal → PrivateVehicleManager ``` /dashboard → "My Vehicles" card click → Flip Modal → PrivateVehicleManager ``` The PrivateVehicleManager shows `VehicleCardStandard` tiles in a carousel. Clicking a tile opens the same `VehicleDetailModal`. ### Path 4: Corporate Mode ``` /organization/:id → /organization/:id/vehicles/:vehicleId ``` All navigation functions in `MyVehiclesCard`, `FleetView`, and `PrivateVehicleManager` check `authStore.isCorporateMode` and route accordingly. --- ## 7. Broken Bindings & Immediate Fixes Required ### 🔴 CRITICAL (Data never renders) | # | Component | Field | Impact | |---|-----------|-------|--------| | 1 | `VehicleCardStandard` | `vehicle.image` | Image area always shows placeholder SVG | | 2 | `VehicleCardStandard` | `vehicle.nickname` | Nickname section always empty | | 3 | `VehicleCardStandard` | `vehicle.countryCode` / `country_code` | Plate badge has no country flag | | 4 | `VehicleCardStandard` | `vehicle.engine` | Engine row falls back to `fuel_type` (acceptable but not ideal) | | 5 | `VehicleCardStandard` | `vehicle.color` | Color row always shows "—" | | 6 | `VehicleCardStandard` | `vehicle.nextService` | Service row always shows "—" | ### 🟡 MODERATE (Fallback works but data is incomplete) | # | Component | Field | Impact | |---|-----------|-------|--------| | 7 | `VehicleCardStandard` | `vehicle.mileage` (deprecated) | Falls back to `current_mileage` — OK | | 8 | `VehicleDetailModal` | `vehicle.body_type` | Falls back to `individual_equipment.car_specs.body_type` — OK | ### 🟢 GREEN (Fully functional) - All fields in the Pinia store `Vehicle` interface match the backend `AssetResponse` - The `fetchVehicles()` endpoint (`GET /assets/vehicles`) is correct - The `fetchAssetFinancials()` endpoint (`GET /assets/vehicles/{id}/financials`) is correct - Cost fetching (`GET /assets/{id}/costs`) is correct - Event fetching (`GET /assets/{id}/events`) is correct - Navigation paths correctly distinguish private vs corporate mode --- ## 8. Recommendations 1. **Add missing fields to `AssetResponse`** (or use `individual_equipment` JSONB): - `nickname` → add to `AssetResponse` as an optional string - `country_code` → already in `individual_equipment`, expose at root level - `next_service_mileage` → computed field or stored in `individual_equipment` 2. **Fix `VehicleCardStandard` to use `individual_equipment.color`** instead of `vehicle.color`: ```typescript vehicle.individual_equipment?.color || vehicle.color || '—' ``` This already works for the color field (line 103) but `vehicle.color` is never set by the API. 3. **Remove `vehicle.image` from `VehicleCardStandard`** or add an image URL field to the backend schema if vehicle images are planned. 4. **Add `country_code` to `AssetResponse`** at the root level so the plate badge can display country flags without digging into JSONB. 5. **Consider adding a `next_service` computed field** to the backend `AssetResponse` or the store getter, derived from `current_mileage` + service interval.