nyelvi refaktorálás előtt

This commit is contained in:
Roo
2026-06-19 09:09:24 +00:00
parent 9ba2d9180d
commit 5b437b220d
17 changed files with 1007 additions and 1088 deletions

File diff suppressed because it is too large Load Diff

View 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.**

View File

@@ -109,9 +109,10 @@ async function fetchAds() {
const res = await api.get<PlacementResponse>(`/marketing/placements/${props.zone}`)
ads.value = res.data.ads || []
} 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) {
ads.value = []
error.value = null
} else {
error.value = 'Failed to load ads'
console.error('[AdPlacementWidget] Error:', err)

View File

@@ -1,8 +1,13 @@
<template>
<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"
@click="$emit('open-card', 'vehicles')"
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"
>
<!-- Invisible overlay to capture clicks on the whole card -->
<div
class="absolute inset-0 z-10 cursor-pointer"
@click="navigateToFleet"
/>
<!-- Top header bar -->
<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>
@@ -15,13 +20,15 @@
v-for="vehicle in displayVehicles.slice(0, 3)"
:key="vehicle.id"
:vehicle="vehicle"
@click="$emit('open-card', 'vehicles')"
@plate-click="openVehicleDetail(vehicle)"
class="relative z-20"
@click.stop="navigateToFleet"
@plate-click.stop="openVehicleDetail(vehicle)"
/>
<!-- Empty state (only when BOTH real and mock are empty) -->
<div
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">
<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">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '../../stores/auth'
import { useVehicleStore } from '../../stores/vehicle'
import type { VehicleData } from '../../types/vehicle'
import VehicleCardCompact from '../vehicle/VehicleCardCompact.vue'
const router = useRouter()
const { t } = useI18n()
const authStore = useAuthStore()
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.
* Sorted by is_primary flag (favorite first), then alphabetically.
@@ -57,7 +63,33 @@ const displayVehicles = computed(() => {
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) {
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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -7,6 +7,12 @@ export default {
optional: 'optional',
select: 'Select',
retry: 'Retry',
back: 'Back',
licensePlate: 'License Plate',
vin: 'VIN',
fuel: 'Fuel',
mileage: 'Mileage',
year: 'Year',
},
subscription: {
title: 'Subscription Plans',
@@ -65,6 +71,48 @@ export default {
addVehicle: '+ Add New Vehicle',
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: {
welcome: 'Welcome',
userMenu: 'User menu',

View File

@@ -7,6 +7,12 @@ export default {
optional: 'opcionális',
select: 'Válassz',
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: {
title: 'Előfizetési Csomagok',
@@ -65,6 +71,45 @@ export default {
addVehicle: '+ Új Jármű Hozzáadása',
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: {
welcome: 'Üdvözlünk',
userMenu: 'Felhasználói menü',

View File

@@ -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"
>
<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
@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"
@@ -125,6 +136,13 @@ function openCompanyData() {
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 ────────────────────────────
function navigateToSubscription() {
isHamburgerOpen.value = false

View File

@@ -146,9 +146,11 @@ const isHamburgerOpen = ref(false)
// ── Navigate to dashboard with card query param ────────────────────
function navigateToCard(cardId: string) {
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
if (cardId === 'costs' || cardId === 'service') {
if (cardId === 'vehicles') {
router.push('/dashboard/vehicles')
} else if (cardId === 'costs' || cardId === 'service') {
router.push('/dashboard')
} else {
router.push({ path: '/dashboard', query: { card: cardId } })

View File

@@ -28,6 +28,16 @@ const router = createRouter({
path: 'subscription',
name: 'subscription',
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',
name: 'org-subscription',
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')
}
]
},

View File

@@ -55,7 +55,6 @@
<!-- Card 1: 🚗 My Garage (Járműveim) -->
<MyVehiclesCard
@open-card="openCard"
@open-vehicle-detail="openVehicleDetail"
/>
<!-- Card 2: 💰 Costs & Quick Actions (Action Center) -->

View 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>

View 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>