13 KiB
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
The /dashboard route is a child route under the PrivateLayout.vue wrapper:
| Path | Name | Component | Auth |
|---|---|---|---|
/dashboard |
dashboard |
DashboardView.vue |
requiresAuth |
/dashboard/vehicles |
FleetView |
FleetView.vue |
requiresAuth |
/dashboard/vehicles/:id |
VehicleDetails |
VehicleDetailsView.vue |
requiresAuth |
/dashboard/service-finder |
service-finder |
ServiceFinderView.vue |
requiresAuth |
/dashboard/subscription |
subscription |
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
Data Fetching (Lifecycle)
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 |
@open-card |
| 2 | CostsActionsCard |
frontend/src/components/dashboard/CostsActionsCard.vue |
@cost-saved |
| 3 | ServiceFinderCard |
frontend/src/components/dashboard/ServiceFinderCard.vue |
— |
| 4 | GamificationCard |
frontend/src/components/dashboard/GamificationCard.vue |
@open-card |
| 5 | ProfileTrustCard |
frontend/src/components/dashboard/ProfileTrustCard.vue |
@open-card |
Modal System
The DashboardView manages 3 modal layers:
- Flip Modal (
activeCard): A 3D-flip overlay that showsPrivateVehicleManagerwhenactiveCard === 'vehicles'. - VehicleDetailModal: Deep-link modal with 4 tabs (Basics, Finance, Alerts, ServiceBook).
- VehicleFormModal: Add/Edit vehicle form (standalone, decoupled from the manager).
3. MyVehiclesCard — The Garage Tile
File: frontend/src/components/dashboard/MyVehiclesCard.vue
Data Source
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 — a minimal row component showing:
license_plate(viaVehiclePlateBadge)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
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
API Endpoint
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. 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
AssetResponsedirectly - Pénzügyek (TCO) — lazy-fetches
GET /assets/{id}/costs - Figyelmeztetések — reads from
individual_equipmentJSONB - 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
Vehicleinterface match the backendAssetResponse - 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
-
Add missing fields to
AssetResponse(or useindividual_equipmentJSONB):nickname→ add toAssetResponseas an optional stringcountry_code→ already inindividual_equipment, expose at root levelnext_service_mileage→ computed field or stored inindividual_equipment
-
Fix
VehicleCardStandardto useindividual_equipment.colorinstead ofvehicle.color:vehicle.individual_equipment?.color || vehicle.color || '—'This already works for the color field (line 103) but
vehicle.coloris never set by the API. -
Remove
vehicle.imagefromVehicleCardStandardor add an image URL field to the backend schema if vehicle images are planned. -
Add
country_codetoAssetResponseat the root level so the plate badge can display country flags without digging into JSONB. -
Consider adding a
next_servicecomputed field to the backendAssetResponseor the store getter, derived fromcurrent_mileage+ service interval.