nyelvi refaktorálás előtt
This commit is contained in:
1115
.roo/history.md
1115
.roo/history.md
File diff suppressed because it is too large
Load Diff
242
docs/i18n_duplicate_key_analysis.md
Normal file
242
docs/i18n_duplicate_key_analysis.md
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# 🌐 i18n Duplikált Kulcsok Összehasonlító Elemzése
|
||||||
|
|
||||||
|
**Dátum:** 2026-06-19
|
||||||
|
**Készítette:** Rendszer-Architect
|
||||||
|
**Cél:** Nyelvi modulok (i18n) kulcsainak felmérése, duplikációk azonosítása, optimalizálási javaslat
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Feltárt Fájlok Áttekintése
|
||||||
|
|
||||||
|
Összesen **8 i18n fájl** található a projektben, két elkülönült rendszerben:
|
||||||
|
|
||||||
|
### 1.1 Frontend (vue-i18n, TypeScript) — `frontend/src/i18n/`
|
||||||
|
|
||||||
|
| Fájl | Sorok | Hatókör | Státusz |
|
||||||
|
|------|-------|---------|---------|
|
||||||
|
| en.ts | ~1603 | Teljes (25 szekció) | ✅ Teljes |
|
||||||
|
| hu.ts | ~1600 | Teljes (25 szekció) | ✅ Teljes |
|
||||||
|
| de.ts | ~83 | Részleges: menu, header, dashboard, zen, auth | ⚠️ Hiányos |
|
||||||
|
| ro.ts | ~83 | Részleges: menu, header, dashboard, zen, auth | ⚠️ Hiányos |
|
||||||
|
| cz.ts | ~83 | Részleges: menu, header, dashboard, zen, auth | ⚠️ Hiányos |
|
||||||
|
| sk.ts | ~83 | Részleges: menu, header, dashboard, zen, auth | ⚠️ Hiányos |
|
||||||
|
|
||||||
|
### 1.2 Backend (statikus JSON) — `backend/static/locales/`
|
||||||
|
|
||||||
|
| Fájl | Sorok | Hatókör | Státusz |
|
||||||
|
|------|-------|---------|---------|
|
||||||
|
| en.json | ~76 | AUTH, SENTINEL, EMAIL, ORGANIZATION | ✅ Teljes |
|
||||||
|
| hu.json | ~83 | AUTH, SENTINEL, EMAIL, ORGANIZATION | ⚠️ Aszimmetrikus |
|
||||||
|
|
||||||
|
> **⚠️ Fontos:** A frontend és backend rendszerek **teljesen elkülönülnek**. Más kulcs-sémát használnak (dot-notation vs UPPER_SNAKE_CASE), így közöttük NINCS duplikáció.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Frontend Kulcsok Hierarchikus Szerkezete
|
||||||
|
|
||||||
|
A frontend i18n rendszer 25 gyökér-szekcióra tagolódik:
|
||||||
|
|
||||||
|
```
|
||||||
|
common (9 kulcs) — Általános (cancel, save, select, year, mileage, stb.)
|
||||||
|
subscription (11) — Előfizetések
|
||||||
|
landing (20+) — Nyitóoldal
|
||||||
|
menu (8) — Navigációs menü
|
||||||
|
garage (18) — Garázs felület
|
||||||
|
vehicleDetail (20+) — Jármű részletek
|
||||||
|
header (35+) — Fejléc
|
||||||
|
dashboard (40+) — Irányítópult
|
||||||
|
finance (25+) — Pénzügyi motor
|
||||||
|
zen (2) — Zen mód
|
||||||
|
auth (50+) — Bejelentkezés, regisztráció
|
||||||
|
profile (60+) — Felhasználói profil
|
||||||
|
kyc (20+) — KYC
|
||||||
|
verifyEmail (15+) — E-mail megerősítés
|
||||||
|
company (70+) — Cég műveletek
|
||||||
|
organization (2) — Szervezeti név
|
||||||
|
address (7) — Cím komponensek
|
||||||
|
servicebook (15+) — Szervizkönyv
|
||||||
|
vehicle (200+) — Jármű adatok
|
||||||
|
serviceFinder (30+) — Szervizkereső
|
||||||
|
provider (60+) — Szolgáltató adatok
|
||||||
|
categories (4) — Kategóriák
|
||||||
|
gamification (2) — Gamifikáció
|
||||||
|
admin.services (20+) — Admin: szolgáltatások
|
||||||
|
admin.packages (30+) — Admin: csomagok
|
||||||
|
admin.users (30+) — Admin: felhasználók
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Duplikált Kulcsok Kategorizált Listája
|
||||||
|
|
||||||
|
### 3.1 ✅ Pontos Egyezések (Biztonságosan Összevonható)
|
||||||
|
|
||||||
|
Ezek a kulcsok **angolban és magyarban is azonos értéket** adnak vissza. Egyetlen közös kulccsá vonhatók össze.
|
||||||
|
|
||||||
|
| # | Érintett Kulcsok | EN érték | HU érték | Előfordulás |
|
||||||
|
|---|-----------------|----------|----------|-------------|
|
||||||
|
| 1 | common.cancel / profile.cancel / servicebook.cancel | "Cancel" | "Mégse" | **3 helyen** |
|
||||||
|
| 2 | common.save / profile.save | "Save" | "Mentés" | **2 helyen** |
|
||||||
|
| 3 | common.saving / servicebook.saving / vehicle.saving / admin.services.saving / admin.packages.saving | "Saving..." | "Mentés..." | **5 helyen** |
|
||||||
|
| 4 | common.back / kyc.back / company.back | "Back" | "Vissza" | **3 helyen** |
|
||||||
|
| 5 | common.mileage / garage.mileage / vehicleDetail.mileage | "Mileage" | "Futásteljesítmény" | **3 helyen** |
|
||||||
|
| 6 | common.year / garage.year | "Year" | "Évjárat" | **2 helyen** |
|
||||||
|
| 7 | common.retry / dashboard.errorRetry | "Retry" | "Újrapróbálkozás" | **2 helyen** |
|
||||||
|
| 8 | garage.noPlate / vehicleDetail.noPlate | "No Plate" | "Nincs rendszám" | **2 helyen** |
|
||||||
|
| 9 | garage.nextService / vehicleDetail.nextService / vehicle.nextService | "Next Service" | "Következő Szerviz" | **3 helyen** |
|
||||||
|
| 10 | vehicleDetail.noData / vehicle.noData | "No data" | "Nincs adat" | **2 helyen** |
|
||||||
|
| 11 | header.subtitle / dashboard.subtitle | "Your dashboard and vehicle overview" | "Irányítópultod és járműveid áttekintése" | **2 helyen ⭐** |
|
||||||
|
| 12 | garage.backToGarage / profile.backToGarage | "Back to Garage" | "Vissza a Garázsba" | **2 helyen** |
|
||||||
|
| 13 | header.profile / profile.title | "Profile Settings" | "Profil beállítások" | **2 helyen** |
|
||||||
|
| 14 | dashboard.statusActive / company.fleetActive / admin.users.status_active | "Active" | "Aktív" | **3 helyen** |
|
||||||
|
| 15 | vehicleDetail.vin / vehicle.label_vin | "VIN (Chassis Number)" | "Alvázszám (VIN)" | **2 helyen** |
|
||||||
|
| 16 | kyc.country / company.country / profile.country | "Country" | "Ország" | **3 helyen** |
|
||||||
|
| 17 | kyc.zipCode / company.zipCode / profile.zipCode | "ZIP Code" | "Irányítószám" | **3 helyen** |
|
||||||
|
| 18 | kyc.city / company.city / profile.city | "City" | "Város" | **3 helyen** |
|
||||||
|
| 19 | kyc.houseNumber / company.houseNumber / profile.houseNumber | "House Number" | "Házszám" | **3 helyen** |
|
||||||
|
| 20 | profile.stairwell / company.stairwell | "Stairwell" | "Lépcsőház" | **2 helyen** |
|
||||||
|
| 21 | profile.floor / company.floor | "Floor" | "Emelet" | **2 helyen** |
|
||||||
|
| 22 | profile.door / company.door | "Door" | "Ajtó" | **2 helyen** |
|
||||||
|
| 23 | profile.parcelId / company.parcelId | "Parcel ID" | "Helyrajzi szám" | **2 helyen** |
|
||||||
|
| 24 | kyc.streetName / company.streetName / provider.streetNameLabel | "Street Name" | "Utca neve" | **3 helyen** |
|
||||||
|
| 25 | kyc.streetType / company.streetType | "Type" | "Típus" | **2 helyen** |
|
||||||
|
|
||||||
|
> ⭐ A **#11** (header.subtitle = dashboard.subtitle) a legszembetűnőbb: **teljesen azonos mondat**, mindkét nyelven. Egyértelműen összevonandó.
|
||||||
|
|
||||||
|
### 3.2 ⚠️ Kis Eltéréssel (Egyedi Kontextus, Megfontolandó)
|
||||||
|
|
||||||
|
| # | Érintett Kulcsok | EN érték | HU érték(ek) | Megjegyzés |
|
||||||
|
|---|-----------------|----------|-------------|------------|
|
||||||
|
| 26 | common.cancel vs company.cancel vs admin.services.cancel | "Cancel" | "Mégse" / "Mégsem" | "Mégsem" elütés? |
|
||||||
|
| 27 | finance.retry vs common.retry vs dashboard.errorRetry | "Retry" | "Újra" / "Újrapróbálkozás" | finance.retry rövidebb |
|
||||||
|
| 28 | vehicleDetail.vin vs vehicle.label_vin | "VIN" / "VIN (Chassis Number)" | "Alvázszám (VIN)" | EN nem azonos |
|
||||||
|
| 29 | company.streetType vs profile.streetType | "Type" / "Street Type" | "Típus" / "Közterület jellege" | Profile formalisabb |
|
||||||
|
| 30 | company.email vs provider.emailLabel | "Email" / "Email" | "E-mail" / "E-mail cím" | Provider explicitabb |
|
||||||
|
| 31 | company.phone vs provider.phoneLabel | "Phone" / "Phone" | "Telefonszám" / "Telefon" | Különböző hossz |
|
||||||
|
| 32 | dashboard.addVehicle vs menu.addVehicle | "+ Add Vehicle" / "+ Add New Vehicle" | "+ Jármű Hozzáadása" / "+ Új Jármű Hozzáadása" | Menü-ben "New" |
|
||||||
|
|
||||||
|
### 3.3 🔄 Jármű Funkciók Átfedései
|
||||||
|
|
||||||
|
A vehicle.features (~130), vehicle.motorcycleFeatures (~70), vehicle.lcvFeatures (~40) és vehicle.hgvFeatures (~90) között jelentős átfedés.
|
||||||
|
|
||||||
|
#### Pontos egyezések (összevonható):
|
||||||
|
|
||||||
|
| Kulcs | EN | HU | Érintett szekciók |
|
||||||
|
|-------|----|----|------------------|
|
||||||
|
| cruise_control | "Cruise Control" | "Tempomat" | features, motorcycleFeatures, lcvFeatures |
|
||||||
|
| alloy_wheels | "Alloy Wheels" | "Könnyűfém felni" | features, lcvFeatures |
|
||||||
|
| garage_kept | "Garage Kept" | "Garázsban tartott" | features, motorcycleFeatures |
|
||||||
|
| xenon_headlights | "Xenon Headlights" | "Xenon fényszóró" | features, lcvFeatures |
|
||||||
|
| fog_lights | "Fog Lights" | "Ködlámpa" | features, lcvFeatures |
|
||||||
|
| sunroof | "Sunroof" | "Napfénytető" | features, lcvFeatures |
|
||||||
|
| heated_seat | "Heated Seat" | "Fűthető ülés" | motorcycleFeatures, lcvFeatures |
|
||||||
|
| navigation | "Navigation (GPS)" | "Navigáció (GPS)" | features, hgvFeatures |
|
||||||
|
| apple_carplay | "Apple CarPlay" | "Apple CarPlay" | features, hgvFeatures |
|
||||||
|
| android_auto | "Android Auto" | "Android Auto" | features, hgvFeatures |
|
||||||
|
| bluetooth | "Bluetooth" | "Bluetooth" | features, hgvFeatures |
|
||||||
|
| wireless_charging | "Wireless Charging" | "Vezeték nélküli töltő" | features, hgvFeatures |
|
||||||
|
| dab_radio | "DAB+ Digital Radio" | "DAB+ digitális rádió" | features, hgvFeatures |
|
||||||
|
| premium_sound | "Premium Sound System" | "Prémium hangrendszer" | features, hgvFeatures |
|
||||||
|
| wifi_hotspot | "WiFi Hotspot" | "WiFi Hotspot" | features, hgvFeatures |
|
||||||
|
| adaptive_cruise | "Adaptive Cruise Control (ACC)" | "Adaptív tempomat (ACC)" | features, hgvFeatures |
|
||||||
|
| 360_camera | "360° Camera" | "360°-os kamera" | features, hgvFeatures |
|
||||||
|
| parking_sensors_rear | "Rear Parking Sensors" | "Hátsó parkolóérzékelők" | features, lcvFeatures |
|
||||||
|
|
||||||
|
#### Eltérő magyar fordítás (manuális átgondolást igényel):
|
||||||
|
|
||||||
|
| Kulcs | EN (features) | EN (másik) | HU (features) | HU (másik) | Forrás |
|
||||||
|
|-------|--------------|------------|---------------|------------|--------|
|
||||||
|
| abs | "ABS" | "ABS" | "ABS" | "ABS (blokkolásgátló)" | motorcycleFeatures |
|
||||||
|
| gps_tracker | "GPS Tracker" | "GPS Tracker" | "GPS követő" | "GPS nyomkövető" | lcvFeatures, hgvFeatures |
|
||||||
|
| alarm | "Alarm System" | "Alarm" | "Riasztórendszer" | "Riasztó" | motorcycleFeatures, lcvFeatures |
|
||||||
|
| first_owner | "First Owner" | "First Owner" | "Első tulajdonos" | "Első tulajdonostól" | motorcycleFeatures |
|
||||||
|
| service_book | "Service Book" | "Service Book" | "Szervizkönyvvel" | "Szervizkönyv" | motorcycleFeatures |
|
||||||
|
| isofix | "ISOFIX" | "ISOFIX Child Seat Anchors" | "Isofix" | "Isofix gyerekülés rögzítés" | lcvFeatures |
|
||||||
|
| tow_bar | "Tow Bar" | "Tow Bar" | "Vontatóhorog (vonóhorog)" | "Vontatóhorog" | lcvFeatures |
|
||||||
|
| usb_charging | "USB Charging" | "USB Charging" | "USB töltőcsatlakozó" | "USB töltő" | hgvFeatures |
|
||||||
|
| subwoofer | "Subwoofer" | "Subwoofer" | "Mélynyomó (Subwoofer)" | "Mélynyomó" | hgvFeatures |
|
||||||
|
| rear_entertainment | "Rear Entertainment" | "Rear Entertainment System" | "Hátsó szórakoztató rendszer" | "Hátsó szórakoztató rendszer" | hgvFeatures |
|
||||||
|
| traction_control | "Traction Control (TCS)" | "Traction Control" | "Kipörgésgátló (TCS)" | "Kipörgésgátló" | hgvFeatures |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Backend JSON Aszimmetria
|
||||||
|
|
||||||
|
### en.json vs hu.json
|
||||||
|
|
||||||
|
| Kulcs | en.json | hu.json | Státusz |
|
||||||
|
|-------|---------|---------|---------|
|
||||||
|
| AUTH.ERROR.UNAUTHORIZED | ✅ "Unauthorized access" | ❌ **HIÁNYZIK** | Pótlandó hu.json-ban |
|
||||||
|
| COMMON.SAVE_SUCCESS | ❌ **HIÁNYZIK** | ✅ "Sikeres mentés!" | Pótlandó en.json-ban |
|
||||||
|
| SENTINEL.APPROVAL.REQUIRED | ❌ **HIÁNYZIK** | ✅ "A kérelem jóváhagyása szükséges" | Pótlandó en.json-ban |
|
||||||
|
|
||||||
|
A fennmaradó ~40 kulcs mindkét fájlban azonos szerkezetű.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Hiányos Fordítások (de, ro, cz, sk)
|
||||||
|
|
||||||
|
A 4 részleges fájl mindegyike **csak 5 szekciót** fed le a 25-ből:
|
||||||
|
|
||||||
|
| Szekció | de.ts | ro.ts | cz.ts | sk.ts |
|
||||||
|
|---------|-------|-------|-------|-------|
|
||||||
|
| menu | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| header | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| dashboard | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| zen | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| auth | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Többi 20 szekció | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
|
||||||
|
**Hiányzó:** common, subscription, landing, garage, vehicleDetail, finance, profile, kyc, verifyEmail, company, organization, address, servicebook, vehicle, serviceFinder, provider, categories, gamification, admin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Statisztikai Összesítés
|
||||||
|
|
||||||
|
| Mérőszám | Érték |
|
||||||
|
|----------|-------|
|
||||||
|
| Összes i18n fájl | **8 db** (6 frontend + 2 backend) |
|
||||||
|
| Frontend teljes kulcsok (en.ts) | **~900+** egyedi kulcs |
|
||||||
|
| Frontend hiányos fájlok | **4 db** (de, ro, cz, sk) — csak 20% lefedettség |
|
||||||
|
| Backend teljes kulcsok | **~55** db |
|
||||||
|
| **Pontos duplikációk (összevonható)** | **25 csoport**, ~60 kulcs érintett |
|
||||||
|
| **Közel-duplikációk (felülvizsgálandó)** | **7 csoport**, ~15 kulcs érintett |
|
||||||
|
| **Jármű feature átfedések (pontos)** | **18 csoport**, ~50 kulcs érintett |
|
||||||
|
| **Jármű feature átfedések (eltérő)** | **11 csoport**, ~25 kulcs érintett |
|
||||||
|
| **Backend hiányzó kulcsok** | **3 db** |
|
||||||
|
| **Összes optimalizálható kulcs** | **~150+** (a ~900-ból) ≈ **16-17%** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Optimalizálási Javaslat
|
||||||
|
|
||||||
|
### 7.1 Azonnal összevonható (25 csoport)
|
||||||
|
|
||||||
|
Javasolt elnevezési konvenció: A leggyakrabban használt kulcsot tartsuk meg (pl. common), a többit alias-ként hivatkozzuk rá.
|
||||||
|
|
||||||
|
### 7.2 Jármű feature-ök konszolidációja
|
||||||
|
|
||||||
|
Javasolt struktúra: egy központi `vehicle.features` szekció, a motorcycleFeatures/lcvFeatures/hgvFeatures pedig hivatkozzon rá. Az eltérő magyar fordításokat egységesíteni kell.
|
||||||
|
|
||||||
|
### 7.3 Backend hiányzó kulcsok pótlása
|
||||||
|
|
||||||
|
- AUTH.ERROR.UNAUTHORIZED → hozzáadandó a hu.json-hoz
|
||||||
|
- COMMON.SAVE_SUCCESS → hozzáadandó az en.json-hoz
|
||||||
|
- SENTINEL.APPROVAL.REQUIRED → hozzáadandó az en.json-hoz
|
||||||
|
|
||||||
|
### 7.4 Várakozó lista (nem sürgős)
|
||||||
|
|
||||||
|
A 4 hiányos fordítást (de, ro, cz, sk) csak az optimalizálás után érdemes bővíteni.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Összefoglaló
|
||||||
|
|
||||||
|
A frontend i18n rendszerben **~150 kulcs (16-17%)** duplikált vagy átfedő. Összevonásukkal:
|
||||||
|
|
||||||
|
- **Karbantarthatóság javul:** egy helyen módosítandó fordítás
|
||||||
|
- **Konzisztencia nő:** nem jelenhet meg kétféleképp ugyanaz
|
||||||
|
- **Fájlméret csökken:** ~5-10% méretcsökkenés
|
||||||
|
- **Backend hiba javul:** 3 hiányzó kulcs pótlása
|
||||||
|
|
||||||
|
**A véglegesítéshez kérjük a felhasználó jóváhagyását.**
|
||||||
@@ -109,9 +109,10 @@ async function fetchAds() {
|
|||||||
const res = await api.get<PlacementResponse>(`/marketing/placements/${props.zone}`)
|
const res = await api.get<PlacementResponse>(`/marketing/placements/${props.zone}`)
|
||||||
ads.value = res.data.ads || []
|
ads.value = res.data.ads || []
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Silently handle 404 (placement not configured yet)
|
// Silently handle 404 (placement not configured yet) — no console noise
|
||||||
if (err.response?.status === 404) {
|
if (err.response?.status === 404) {
|
||||||
ads.value = []
|
ads.value = []
|
||||||
|
error.value = null
|
||||||
} else {
|
} else {
|
||||||
error.value = 'Failed to load ads'
|
error.value = 'Failed to load ads'
|
||||||
console.error('[AdPlacementWidget] Error:', err)
|
console.error('[AdPlacementWidget] Error:', err)
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="bg-white/95 rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.12)] overflow-hidden flex flex-col h-[350px] relative cursor-pointer transition-all duration-300 ease-out transform hover:-translate-y-3 hover:scale-[1.02] hover:shadow-2xl"
|
class="bg-white/95 rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.12)] overflow-hidden flex flex-col h-[350px] relative transition-all duration-300 ease-out transform hover:-translate-y-3 hover:scale-[1.02] hover:shadow-2xl"
|
||||||
@click="$emit('open-card', 'vehicles')"
|
|
||||||
>
|
>
|
||||||
|
<!-- Invisible overlay to capture clicks on the whole card -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 z-10 cursor-pointer"
|
||||||
|
@click="navigateToFleet"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Top header bar -->
|
<!-- Top header bar -->
|
||||||
<div class="h-12 bg-slate-700 w-full shrink-0 flex items-center px-4">
|
<div class="h-12 bg-slate-700 w-full shrink-0 flex items-center px-4">
|
||||||
<span class="text-white font-bold text-sm tracking-wide">🚗 {{ t('dashboard.myVehicles') }}</span>
|
<span class="text-white font-bold text-sm tracking-wide">🚗 {{ t('dashboard.myVehicles') }}</span>
|
||||||
@@ -15,13 +20,15 @@
|
|||||||
v-for="vehicle in displayVehicles.slice(0, 3)"
|
v-for="vehicle in displayVehicles.slice(0, 3)"
|
||||||
:key="vehicle.id"
|
:key="vehicle.id"
|
||||||
:vehicle="vehicle"
|
:vehicle="vehicle"
|
||||||
@click="$emit('open-card', 'vehicles')"
|
class="relative z-20"
|
||||||
@plate-click="openVehicleDetail(vehicle)"
|
@click.stop="navigateToFleet"
|
||||||
|
@plate-click.stop="openVehicleDetail(vehicle)"
|
||||||
/>
|
/>
|
||||||
<!-- Empty state (only when BOTH real and mock are empty) -->
|
<!-- Empty state (only when BOTH real and mock are empty) -->
|
||||||
<div
|
<div
|
||||||
v-if="displayVehicles.length === 0"
|
v-if="displayVehicles.length === 0"
|
||||||
class="flex flex-col items-center justify-center py-6 text-slate-400"
|
class="flex flex-col items-center justify-center py-6 text-slate-400 relative z-20"
|
||||||
|
@click.stop="navigateToFleet"
|
||||||
>
|
>
|
||||||
<svg class="w-10 h-10 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-10 h-10 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
@@ -35,19 +42,18 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { useVehicleStore } from '../../stores/vehicle'
|
import { useVehicleStore } from '../../stores/vehicle'
|
||||||
import type { VehicleData } from '../../types/vehicle'
|
import type { VehicleData } from '../../types/vehicle'
|
||||||
import VehicleCardCompact from '../vehicle/VehicleCardCompact.vue'
|
import VehicleCardCompact from '../vehicle/VehicleCardCompact.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const authStore = useAuthStore()
|
||||||
const vehicleStore = useVehicleStore()
|
const vehicleStore = useVehicleStore()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'open-card': [cardId: string, targetId?: string | null]
|
|
||||||
'open-vehicle-detail': [vehicle: VehicleData]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display vehicles from the backend API only.
|
* Display vehicles from the backend API only.
|
||||||
* Sorted by is_primary flag (favorite first), then alphabetically.
|
* Sorted by is_primary flag (favorite first), then alphabetically.
|
||||||
@@ -57,7 +63,33 @@ const displayVehicles = computed(() => {
|
|||||||
return vehicleStore.sortedVehicles
|
return vehicleStore.sortedVehicles
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate directly to the Vehicle Details page.
|
||||||
|
* Dynamically decides the route based on the user's current context:
|
||||||
|
* - Corporate mode → /organization/:id/vehicles/:vehicleId
|
||||||
|
* - Private mode → /dashboard/vehicles/:vehicleId
|
||||||
|
*/
|
||||||
function openVehicleDetail(vehicle: VehicleData) {
|
function openVehicleDetail(vehicle: VehicleData) {
|
||||||
emit('open-vehicle-detail', vehicle)
|
const vehicleId = vehicle.id
|
||||||
|
if (authStore.isCorporateMode && authStore.user?.active_organization_id) {
|
||||||
|
router.push(`/organization/${authStore.user.active_organization_id}/vehicles/${vehicleId}`)
|
||||||
|
} else {
|
||||||
|
router.push(`/dashboard/vehicles/${vehicleId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the Fleet View (Garage) page.
|
||||||
|
* Dynamically decides the route based on the user's current context:
|
||||||
|
* - Corporate mode → /organization/:id/vehicles
|
||||||
|
* - Private mode → /dashboard/vehicles
|
||||||
|
*/
|
||||||
|
function navigateToFleet() {
|
||||||
|
if (authStore.isCorporateMode && authStore.user?.active_organization_id) {
|
||||||
|
router.push(`/organization/${authStore.user.active_organization_id}/vehicles`)
|
||||||
|
} else {
|
||||||
|
// FleetView is a child route under /dashboard
|
||||||
|
router.push('/dashboard/vehicles')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
16
frontend/src/components/vehicles/tabs/DocumentsTab.vue
Normal file
16
frontend/src/components/vehicles/tabs/DocumentsTab.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur-sm">
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-4">{{ t('vehicleDetail.tabDocuments') }}</h3>
|
||||||
|
<div class="flex flex-col items-center justify-center py-12 text-white/50">
|
||||||
|
<svg class="mb-4 h-16 w-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-base">{{ t('vehicleDetail.placeholderDocuments') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
const { t } = useI18n()
|
||||||
|
</script>
|
||||||
16
frontend/src/components/vehicles/tabs/FinancialsTab.vue
Normal file
16
frontend/src/components/vehicles/tabs/FinancialsTab.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur-sm">
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-4">{{ t('vehicleDetail.tabFinancials') }}</h3>
|
||||||
|
<div class="flex flex-col items-center justify-center py-12 text-white/50">
|
||||||
|
<svg class="mb-4 h-16 w-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-base">{{ t('vehicleDetail.placeholderFinancials') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
const { t } = useI18n()
|
||||||
|
</script>
|
||||||
165
frontend/src/components/vehicles/tabs/OverviewTab.vue
Normal file
165
frontend/src/components/vehicles/tabs/OverviewTab.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- ═══ Row 1: Photo + Basic Info ═══ -->
|
||||||
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||||
|
<!-- ── Vehicle Photo ── -->
|
||||||
|
<div class="flex aspect-[16/9] items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-white/5 backdrop-blur-sm lg:aspect-square">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1503376712341-a67b5e40e2d1?auto=format&fit=crop&w=800&q=80"
|
||||||
|
alt="Vehicle photo"
|
||||||
|
class="h-full w-full object-cover rounded-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Basic Info ── -->
|
||||||
|
<div class="flex flex-col justify-center rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur-sm lg:col-span-2">
|
||||||
|
<!-- License Plate (large, prominent) -->
|
||||||
|
<h2 class="text-4xl font-bold tracking-wider text-white">
|
||||||
|
{{ vehicle?.license_plate || t('vehicleDetail.noPlate') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Brand / Model -->
|
||||||
|
<p class="mt-2 text-2xl text-white/80">
|
||||||
|
{{ vehicle?.brand || '—' }} {{ vehicle?.model || '—' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Year of Manufacture -->
|
||||||
|
<p class="mt-1 text-lg text-white/50">
|
||||||
|
{{ vehicle?.year_of_manufacture || '—' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<div class="my-4 border-t border-white/10" />
|
||||||
|
|
||||||
|
<!-- Quick specs row -->
|
||||||
|
<div class="flex flex-wrap gap-x-8 gap-y-2">
|
||||||
|
<div>
|
||||||
|
<span class="text-xs uppercase tracking-wider text-white/40">{{ t('vehicleDetail.vin') }}</span>
|
||||||
|
<p class="text-sm text-white/70">{{ vehicle?.vin || '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs uppercase tracking-wider text-white/40">{{ t('vehicleDetail.fuelType') }}</span>
|
||||||
|
<p class="text-sm text-white/70">{{ fuelLabel }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs uppercase tracking-wider text-white/40">{{ t('vehicleDetail.mileage') }}</span>
|
||||||
|
<p class="text-sm text-white/70">{{ formattedMileage }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ Row 2: Vital Signs (3 cards) ═══ -->
|
||||||
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||||
|
<!-- ── MOT Expiry ── -->
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 backdrop-blur-sm">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-amber-500/20">
|
||||||
|
<svg class="h-5 w-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs uppercase tracking-wider text-white/40">{{ t('vehicleDetail.motExpiry') }}</p>
|
||||||
|
<p class="text-base font-semibold text-white" :class="motStatusClass">
|
||||||
|
{{ motExpiryLabel }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Current Mileage ── -->
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 backdrop-blur-sm">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-500/20">
|
||||||
|
<svg class="h-5 w-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs uppercase tracking-wider text-white/40">{{ t('vehicleDetail.mileage') }}</p>
|
||||||
|
<p class="text-base font-semibold text-white">
|
||||||
|
{{ formattedMileage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Next Service ── -->
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 backdrop-blur-sm">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/20">
|
||||||
|
<svg class="h-5 w-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs uppercase tracking-wider text-white/40">{{ t('vehicleDetail.nextService') }}</p>
|
||||||
|
<p class="text-base font-semibold text-white">
|
||||||
|
{{ nextServiceLabel }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, inject } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import type { Vehicle } from '../../../stores/vehicle'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// ── Inject vehicle from parent (provided by VehicleDetailsView) ──
|
||||||
|
const vehicle = inject<computed<Vehicle | null>>('vehicle')
|
||||||
|
|
||||||
|
// ── Fuel type label ──
|
||||||
|
const fuelLabel = computed(() => {
|
||||||
|
if (!vehicle?.value?.fuel_type) return '—'
|
||||||
|
const key = `vehicle.fuelTypes.${vehicle.value.fuel_type}`
|
||||||
|
const translated = t(key)
|
||||||
|
return translated !== key ? translated : vehicle.value.fuel_type
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Formatted mileage ──
|
||||||
|
const formattedMileage = computed(() => {
|
||||||
|
if (vehicle?.value?.current_mileage == null) return '—'
|
||||||
|
return `${Number(vehicle.value.current_mileage).toLocaleString()} km`
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── MOT Expiry ──
|
||||||
|
const motExpiryLabel = computed(() => {
|
||||||
|
const dates = vehicle?.value?.individual_equipment?.dates
|
||||||
|
if (!dates?.mot_expiry) return t('vehicleDetail.noData')
|
||||||
|
return formatDate(dates.mot_expiry)
|
||||||
|
})
|
||||||
|
|
||||||
|
const motStatusClass = computed(() => {
|
||||||
|
const dates = vehicle?.value?.individual_equipment?.dates
|
||||||
|
if (!dates?.mot_expiry) return ''
|
||||||
|
const expiry = new Date(dates.mot_expiry)
|
||||||
|
const now = new Date()
|
||||||
|
const diffDays = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||||
|
if (diffDays < 0) return 'text-red-400'
|
||||||
|
if (diffDays < 30) return 'text-amber-400'
|
||||||
|
return 'text-emerald-400'
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Next Service ──
|
||||||
|
const nextServiceLabel = computed(() => {
|
||||||
|
// Placeholder — will be connected to the service book / maintenance schedule later
|
||||||
|
return t('vehicleDetail.comingSoon')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Date formatter helper ──
|
||||||
|
function formatDate(dateStr: string): string {
|
||||||
|
try {
|
||||||
|
const d = new Date(dateStr)
|
||||||
|
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
|
||||||
|
} catch {
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
16
frontend/src/components/vehicles/tabs/ServiceBookTab.vue
Normal file
16
frontend/src/components/vehicles/tabs/ServiceBookTab.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur-sm">
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-4">{{ t('vehicleDetail.tabServiceBook') }}</h3>
|
||||||
|
<div class="flex flex-col items-center justify-center py-12 text-white/50">
|
||||||
|
<svg class="mb-4 h-16 w-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-base">{{ t('vehicleDetail.placeholderServiceBook') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
const { t } = useI18n()
|
||||||
|
</script>
|
||||||
17
frontend/src/components/vehicles/tabs/TechDataTab.vue
Normal file
17
frontend/src/components/vehicles/tabs/TechDataTab.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur-sm">
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-4">{{ t('vehicleDetail.tabTechData') }}</h3>
|
||||||
|
<div class="flex flex-col items-center justify-center py-12 text-white/50">
|
||||||
|
<svg class="mb-4 h-16 w-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-base">{{ t('vehicleDetail.placeholderTechData') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
const { t } = useI18n()
|
||||||
|
</script>
|
||||||
@@ -7,6 +7,12 @@ export default {
|
|||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
retry: 'Retry',
|
retry: 'Retry',
|
||||||
|
back: 'Back',
|
||||||
|
licensePlate: 'License Plate',
|
||||||
|
vin: 'VIN',
|
||||||
|
fuel: 'Fuel',
|
||||||
|
mileage: 'Mileage',
|
||||||
|
year: 'Year',
|
||||||
},
|
},
|
||||||
subscription: {
|
subscription: {
|
||||||
title: 'Subscription Plans',
|
title: 'Subscription Plans',
|
||||||
@@ -65,6 +71,48 @@ export default {
|
|||||||
addVehicle: '+ Add New Vehicle',
|
addVehicle: '+ Add New Vehicle',
|
||||||
service: 'Service',
|
service: 'Service',
|
||||||
},
|
},
|
||||||
|
garage: {
|
||||||
|
title: 'My Garage',
|
||||||
|
subtitle: 'Managing {count} vehicles',
|
||||||
|
backToDashboard: 'Back to Dashboard',
|
||||||
|
backToGarage: 'Back to Garage',
|
||||||
|
noPlate: 'No Plate',
|
||||||
|
primary: 'Primary',
|
||||||
|
brandModel: 'Brand / Model',
|
||||||
|
year: 'Year',
|
||||||
|
nextService: 'Next Service',
|
||||||
|
mileage: 'Mileage',
|
||||||
|
emptyTitle: 'Your garage is empty',
|
||||||
|
emptySubtitle: 'Add your first vehicle to get started!',
|
||||||
|
},
|
||||||
|
vehicleDetail: {
|
||||||
|
title: 'Vehicle Details',
|
||||||
|
placeholderTitle: 'Deep Dive Coming Soon',
|
||||||
|
placeholderSubtitle: 'This page will soon feature a full tabbed layout with detailed vehicle information, service history, costs, and more.',
|
||||||
|
notFound: 'Vehicle not found.',
|
||||||
|
noPlate: 'No Plate',
|
||||||
|
// Tab labels
|
||||||
|
tabOverview: 'Overview',
|
||||||
|
tabTechData: 'Technical Data',
|
||||||
|
tabServiceBook: 'Service Book',
|
||||||
|
tabFinancials: 'Financials',
|
||||||
|
tabDocuments: 'Documents',
|
||||||
|
// Tab placeholders
|
||||||
|
placeholderOverview: 'Vehicle overview details will appear here.',
|
||||||
|
placeholderTechData: 'Technical specifications will appear here.',
|
||||||
|
placeholderServiceBook: 'Service history will appear here.',
|
||||||
|
placeholderFinancials: 'Financial records will appear here.',
|
||||||
|
placeholderDocuments: 'Documents will appear here.',
|
||||||
|
// OverviewTab keys
|
||||||
|
photoPlaceholder: 'Photo upload coming soon',
|
||||||
|
vin: 'VIN',
|
||||||
|
fuelType: 'Fuel',
|
||||||
|
mileage: 'Mileage',
|
||||||
|
motExpiry: 'MOT Expiry',
|
||||||
|
nextService: 'Next Service',
|
||||||
|
noData: 'No data',
|
||||||
|
comingSoon: 'Coming soon',
|
||||||
|
},
|
||||||
header: {
|
header: {
|
||||||
welcome: 'Welcome',
|
welcome: 'Welcome',
|
||||||
userMenu: 'User menu',
|
userMenu: 'User menu',
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ export default {
|
|||||||
optional: 'opcionális',
|
optional: 'opcionális',
|
||||||
select: 'Válassz',
|
select: 'Válassz',
|
||||||
retry: 'Újrapróbálkozás',
|
retry: 'Újrapróbálkozás',
|
||||||
|
back: 'Vissza',
|
||||||
|
licensePlate: 'Rendszám',
|
||||||
|
vin: 'Alvázszám (VIN)',
|
||||||
|
fuel: 'Üzemanyag',
|
||||||
|
mileage: 'Futásteljesítmény',
|
||||||
|
year: 'Évjárat',
|
||||||
},
|
},
|
||||||
subscription: {
|
subscription: {
|
||||||
title: 'Előfizetési Csomagok',
|
title: 'Előfizetési Csomagok',
|
||||||
@@ -65,6 +71,45 @@ export default {
|
|||||||
addVehicle: '+ Új Jármű Hozzáadása',
|
addVehicle: '+ Új Jármű Hozzáadása',
|
||||||
service: 'Szervíz',
|
service: 'Szervíz',
|
||||||
},
|
},
|
||||||
|
garage: {
|
||||||
|
title: 'Garázsom',
|
||||||
|
subtitle: '{count} jármű kezelése',
|
||||||
|
backToDashboard: 'Vissza az irányítópultra',
|
||||||
|
backToGarage: 'Vissza a Garázsba',
|
||||||
|
noPlate: 'Nincs rendszám',
|
||||||
|
primary: 'Elsődleges',
|
||||||
|
brandModel: 'Márka / Modell',
|
||||||
|
year: 'Évjárat',
|
||||||
|
nextService: 'Következő Szerviz',
|
||||||
|
mileage: 'Futásteljesítmény',
|
||||||
|
emptyTitle: 'A garázsod üres',
|
||||||
|
emptySubtitle: 'Add hozzá az első járművedet!',
|
||||||
|
},
|
||||||
|
vehicleDetail: {
|
||||||
|
title: 'Jármű Részletek',
|
||||||
|
placeholderTitle: 'Részletes nézet hamarosan',
|
||||||
|
placeholderSubtitle: 'Ez az oldal hamarosan teljes tabokkal ellátott részletes járműinformációkat, szerviztörténetet, költségeket és egyebeket tartalmaz.',
|
||||||
|
notFound: 'A jármű nem található.',
|
||||||
|
noPlate: 'Nincs rendszám',
|
||||||
|
tabOverview: 'Áttekintés',
|
||||||
|
tabTechData: 'Műszaki adatok',
|
||||||
|
tabServiceBook: 'Szervizkönyv',
|
||||||
|
tabFinancials: 'Pénzügyek',
|
||||||
|
tabDocuments: 'Dokumentumok',
|
||||||
|
placeholderOverview: 'A jármű áttekintő adatai itt jelennek meg.',
|
||||||
|
placeholderTechData: 'A műszaki adatok itt jelennek meg.',
|
||||||
|
placeholderServiceBook: 'A szerviztörténet itt jelenik meg.',
|
||||||
|
placeholderFinancials: 'A pénzügyi adatok itt jelennek meg.',
|
||||||
|
placeholderDocuments: 'A dokumentumok itt jelennek meg.',
|
||||||
|
photoPlaceholder: 'Fotó feltöltés hamarosan',
|
||||||
|
vin: 'Alvázszám (VIN)',
|
||||||
|
fuelType: 'Üzemanyag',
|
||||||
|
mileage: 'Futásteljesítmény',
|
||||||
|
motExpiry: 'Műszaki érvényesség',
|
||||||
|
nextService: 'Következő Szerviz',
|
||||||
|
noData: 'Nincs adat',
|
||||||
|
comingSoon: 'Hamarosan',
|
||||||
|
},
|
||||||
header: {
|
header: {
|
||||||
welcome: 'Üdvözlünk',
|
welcome: 'Üdvözlünk',
|
||||||
userMenu: 'Felhasználói menü',
|
userMenu: 'Felhasználói menü',
|
||||||
|
|||||||
@@ -38,6 +38,17 @@
|
|||||||
class="absolute right-0 top-full mt-2 w-56 z-50 rounded-xl border border-white/10 bg-[#04151F]/95 backdrop-blur-xl shadow-2xl shadow-black/40 overflow-hidden"
|
class="absolute right-0 top-full mt-2 w-56 z-50 rounded-xl border border-white/10 bg-[#04151F]/95 backdrop-blur-xl shadow-2xl shadow-black/40 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="py-1">
|
<div class="py-1">
|
||||||
|
<!-- Járművek (Fleet) -->
|
||||||
|
<button
|
||||||
|
@click="navigateToVehicles"
|
||||||
|
class="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-white/80 transition-all duration-150 hover:bg-white/5 hover:text-white"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
{{ t('menu.garage') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="openCompanyData"
|
@click="openCompanyData"
|
||||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-white/80 transition-all duration-150 hover:bg-white/5 hover:text-white"
|
class="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-white/80 transition-all duration-150 hover:bg-white/5 hover:text-white"
|
||||||
@@ -125,6 +136,13 @@ function openCompanyData() {
|
|||||||
showCompanyDataModal.value = true
|
showCompanyDataModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Navigate to Vehicles (Fleet) page ──────────────────────────────
|
||||||
|
function navigateToVehicles() {
|
||||||
|
isHamburgerOpen.value = false
|
||||||
|
const orgId = route.params.id
|
||||||
|
router.push(`/organization/${orgId}/vehicles`)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Navigate to Subscription Plans page ────────────────────────────
|
// ── Navigate to Subscription Plans page ────────────────────────────
|
||||||
function navigateToSubscription() {
|
function navigateToSubscription() {
|
||||||
isHamburgerOpen.value = false
|
isHamburgerOpen.value = false
|
||||||
|
|||||||
@@ -146,9 +146,11 @@ const isHamburgerOpen = ref(false)
|
|||||||
// ── Navigate to dashboard with card query param ────────────────────
|
// ── Navigate to dashboard with card query param ────────────────────
|
||||||
function navigateToCard(cardId: string) {
|
function navigateToCard(cardId: string) {
|
||||||
isHamburgerOpen.value = false
|
isHamburgerOpen.value = false
|
||||||
// 'vehicles' opens the flip modal with PrivateVehicleManager
|
// 'vehicles' navigates to the full FleetView page
|
||||||
// 'costs' and 'service' navigate to dashboard where their cards are already visible inline
|
// 'costs' and 'service' navigate to dashboard where their cards are already visible inline
|
||||||
if (cardId === 'costs' || cardId === 'service') {
|
if (cardId === 'vehicles') {
|
||||||
|
router.push('/dashboard/vehicles')
|
||||||
|
} else if (cardId === 'costs' || cardId === 'service') {
|
||||||
router.push('/dashboard')
|
router.push('/dashboard')
|
||||||
} else {
|
} else {
|
||||||
router.push({ path: '/dashboard', query: { card: cardId } })
|
router.push({ path: '/dashboard', query: { card: cardId } })
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ const router = createRouter({
|
|||||||
path: 'subscription',
|
path: 'subscription',
|
||||||
name: 'subscription',
|
name: 'subscription',
|
||||||
component: () => import('../views/SubscriptionPlansView.vue')
|
component: () => import('../views/SubscriptionPlansView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'vehicles',
|
||||||
|
name: 'FleetView',
|
||||||
|
component: () => import('../views/vehicles/FleetView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'vehicles/:id',
|
||||||
|
name: 'VehicleDetails',
|
||||||
|
component: () => import('../views/vehicles/VehicleDetailsView.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -45,6 +55,16 @@ const router = createRouter({
|
|||||||
path: 'subscription',
|
path: 'subscription',
|
||||||
name: 'org-subscription',
|
name: 'org-subscription',
|
||||||
component: () => import('../views/SubscriptionPlansView.vue')
|
component: () => import('../views/SubscriptionPlansView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'vehicles',
|
||||||
|
name: 'OrgFleetView',
|
||||||
|
component: () => import('../views/vehicles/FleetView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'vehicles/:id',
|
||||||
|
name: 'OrgVehicleDetails',
|
||||||
|
component: () => import('../views/vehicles/VehicleDetailsView.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,7 +55,6 @@
|
|||||||
<!-- Card 1: 🚗 My Garage (Járműveim) -->
|
<!-- Card 1: 🚗 My Garage (Járműveim) -->
|
||||||
<MyVehiclesCard
|
<MyVehiclesCard
|
||||||
@open-card="openCard"
|
@open-card="openCard"
|
||||||
@open-vehicle-detail="openVehicleDetail"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Card 2: 💰 Costs & Quick Actions (Action Center) -->
|
<!-- Card 2: 💰 Costs & Quick Actions (Action Center) -->
|
||||||
|
|||||||
150
frontend/src/views/vehicles/FleetView.vue
Normal file
150
frontend/src/views/vehicles/FleetView.vue
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative min-h-screen">
|
||||||
|
<!-- Garage Background Image (no dark overlay) -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<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('garage.title') }}</h1>
|
||||||
|
<p class="mt-1 text-sm text-white/60">
|
||||||
|
{{ t('garage.subtitle', { count: vehicleStore.vehicles.length }) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- Back to Dashboard button -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Loading state -->
|
||||||
|
<div
|
||||||
|
v-if="vehicleStore.isLoading"
|
||||||
|
class="flex items-center justify-center py-20"
|
||||||
|
>
|
||||||
|
<div class="h-10 w-10 animate-spin rounded-full border-4 border-white/10 border-t-[#70BC84]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state -->
|
||||||
|
<div
|
||||||
|
v-else-if="vehicleStore.vehicles.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center py-20 text-white/50"
|
||||||
|
>
|
||||||
|
<svg class="mb-4 h-16 w-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-lg font-medium">{{ t('garage.emptyTitle') }}</p>
|
||||||
|
<p class="mt-1 text-sm">{{ t('garage.emptySubtitle') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ Vehicle Grid ═══ -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||||
|
>
|
||||||
|
<BaseCard
|
||||||
|
v-for="vehicle in vehicleStore.sortedVehicles"
|
||||||
|
:key="vehicle.id"
|
||||||
|
:hoverable="true"
|
||||||
|
:clickable="true"
|
||||||
|
height="h-[280px]"
|
||||||
|
headerColor="bg-slate-700"
|
||||||
|
@click="openVehicleDetail(vehicle.id)"
|
||||||
|
>
|
||||||
|
<!-- Header slot: License plate -->
|
||||||
|
<template #header>
|
||||||
|
<div class="flex w-full items-center justify-between">
|
||||||
|
<span class="text-white font-bold text-sm tracking-wide truncate">
|
||||||
|
{{ vehicle.license_plate || t('garage.noPlate') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="vehicle.is_primary" class="ml-2 rounded-full bg-yellow-400/20 px-2 py-0.5 text-[10px] font-semibold text-yellow-300">
|
||||||
|
{{ t('garage.primary') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Default slot: Vehicle info -->
|
||||||
|
<div class="flex flex-col gap-3 p-2">
|
||||||
|
<!-- Vehicle icon / image placeholder -->
|
||||||
|
<div class="flex items-center justify-center rounded-xl bg-slate-100 py-4">
|
||||||
|
<svg class="h-12 w-12 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Key details -->
|
||||||
|
<div class="space-y-1.5 text-sm">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-500">{{ t('garage.brandModel') }}</span>
|
||||||
|
<span class="font-medium text-slate-800">
|
||||||
|
{{ vehicle.brand || '—' }} {{ vehicle.model || '' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-500">{{ t('garage.year') }}</span>
|
||||||
|
<span class="font-medium text-slate-800">
|
||||||
|
{{ vehicle.year_of_manufacture || vehicle.year || '—' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-500">{{ t('garage.nextService') }}</span>
|
||||||
|
<span class="font-medium text-slate-800">
|
||||||
|
{{ vehicle.individual_equipment?.dates?.mot_expiry || '—' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-slate-500">{{ t('garage.mileage') }}</span>
|
||||||
|
<span class="font-medium text-slate-800">
|
||||||
|
{{ vehicle.current_mileage ? `${vehicle.current_mileage.toLocaleString()} km` : '—' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
import { useVehicleStore } from '../../stores/vehicle'
|
||||||
|
import BaseCard from '../../components/ui/BaseCard.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const vehicleStore = useVehicleStore()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the Vehicle Details page.
|
||||||
|
* Respects corporate vs private context.
|
||||||
|
*/
|
||||||
|
function openVehicleDetail(vehicleId: string) {
|
||||||
|
if (authStore.isCorporateMode && authStore.user?.active_organization_id) {
|
||||||
|
router.push(`/organization/${authStore.user.active_organization_id}/vehicles/${vehicleId}`)
|
||||||
|
} else {
|
||||||
|
router.push(`/dashboard/vehicles/${vehicleId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Ensure vehicles are loaded when navigating directly to this page
|
||||||
|
if (vehicleStore.vehicles.length === 0) {
|
||||||
|
vehicleStore.fetchVehicles()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
161
frontend/src/views/vehicles/VehicleDetailsView.vue
Normal file
161
frontend/src/views/vehicles/VehicleDetailsView.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative min-h-screen">
|
||||||
|
<!-- Garage background image -->
|
||||||
|
<div class="fixed inset-0 z-0 bg-[url('@/assets/garage-bg.png')] bg-cover bg-center bg-fixed"></div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="relative z-10 mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
<!-- Back button -->
|
||||||
|
<button
|
||||||
|
@click="goBack"
|
||||||
|
class="mb-6 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>
|
||||||
|
|
||||||
|
<!-- ═══ Loading State ═══ -->
|
||||||
|
<div v-if="isLoading" class="flex flex-col items-center justify-center py-24">
|
||||||
|
<svg class="h-10 w-10 animate-spin text-white/40" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
<p class="mt-4 text-sm text-white/50">{{ t('dashboard.loading') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ Error State ═══ -->
|
||||||
|
<div v-else-if="!vehicle" class="flex flex-col items-center justify-center py-24 text-white/50">
|
||||||
|
<svg class="mb-4 h-16 w-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-lg">{{ t('vehicleDetail.notFound') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ Vehicle Loaded ═══ -->
|
||||||
|
<template v-if="vehicle">
|
||||||
|
<!-- Vehicle header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-3xl font-bold text-white">
|
||||||
|
{{ vehicle.license_plate || t('vehicleDetail.title') }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-sm text-white/60">
|
||||||
|
{{ vehicle.brand }} {{ vehicle.model }}
|
||||||
|
<span v-if="vehicle.year_of_manufacture"> · {{ vehicle.year_of_manufacture }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ Tab Navigation ═══ -->
|
||||||
|
<div class="mb-6 flex flex-wrap gap-1 border-b border-white/10">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabDefinitions"
|
||||||
|
:key="tab.key"
|
||||||
|
@click="activeTab = tab.key"
|
||||||
|
class="relative px-5 py-3 text-sm font-medium transition-colors duration-200"
|
||||||
|
:class="activeTab === tab.key
|
||||||
|
? 'text-white'
|
||||||
|
: 'text-white/50 hover:text-white/80'"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
<!-- Active indicator line -->
|
||||||
|
<span
|
||||||
|
v-if="activeTab === tab.key"
|
||||||
|
class="absolute bottom-0 left-0 right-0 h-0.5 bg-[#70BC84]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ Tab Content (dynamic component) ═══ -->
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<component :is="activeComponent" :key="activeTab" />
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onMounted, markRaw, provide } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
import { useVehicleStore } from '../../stores/vehicle'
|
||||||
|
|
||||||
|
// ── Tab Components ──
|
||||||
|
import OverviewTab from '../../components/vehicles/tabs/OverviewTab.vue'
|
||||||
|
import TechDataTab from '../../components/vehicles/tabs/TechDataTab.vue'
|
||||||
|
import ServiceBookTab from '../../components/vehicles/tabs/ServiceBookTab.vue'
|
||||||
|
import FinancialsTab from '../../components/vehicles/tabs/FinancialsTab.vue'
|
||||||
|
import DocumentsTab from '../../components/vehicles/tabs/DocumentsTab.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const vehicleStore = useVehicleStore()
|
||||||
|
|
||||||
|
// ── Provide vehicle data to child tab components ──
|
||||||
|
const vehicle = computed(() => {
|
||||||
|
const id = route.params.id as string
|
||||||
|
return vehicleStore.vehicles.find(v => v.id === id) || null
|
||||||
|
})
|
||||||
|
provide('vehicle', vehicle)
|
||||||
|
|
||||||
|
// ── Loading state ──
|
||||||
|
const isLoading = computed(() => {
|
||||||
|
return vehicleStore.isLoading || (vehicleStore.vehicles.length === 0 && !vehicle.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Tab definitions (fully reactive via computed) ──
|
||||||
|
interface TabDefinition {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
component: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabDefinitions = computed<TabDefinition[]>(() => [
|
||||||
|
{ key: 'overview', label: t('vehicleDetail.tabOverview'), component: markRaw(OverviewTab) },
|
||||||
|
{ key: 'techdata', label: t('vehicleDetail.tabTechData'), component: markRaw(TechDataTab) },
|
||||||
|
{ key: 'servicebook', label: t('vehicleDetail.tabServiceBook'), component: markRaw(ServiceBookTab) },
|
||||||
|
{ key: 'financials', label: t('vehicleDetail.tabFinancials'), component: markRaw(FinancialsTab) },
|
||||||
|
{ key: 'documents', label: t('vehicleDetail.tabDocuments'), component: markRaw(DocumentsTab) },
|
||||||
|
])
|
||||||
|
|
||||||
|
const activeTab = ref<string>('overview')
|
||||||
|
|
||||||
|
const activeComponent = computed(() => {
|
||||||
|
const found = tabDefinitions.value.find(t => t.key === activeTab.value)
|
||||||
|
return found ? found.component : OverviewTab
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate back to the fleet view.
|
||||||
|
* Respects corporate vs private context.
|
||||||
|
*/
|
||||||
|
function goBack() {
|
||||||
|
if (authStore.isCorporateMode && authStore.user?.active_organization_id) {
|
||||||
|
router.push(`/organization/${authStore.user.active_organization_id}/vehicles`)
|
||||||
|
} else {
|
||||||
|
router.push('/dashboard/vehicles')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Ensure vehicles are loaded — critical for direct navigation (F5 / URL paste)
|
||||||
|
if (vehicleStore.vehicles.length === 0) {
|
||||||
|
await vehicleStore.fetchVehicles()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user