RABC fejlesztése és beélesítése hibák kijavításával

This commit is contained in:
Roo
2026-06-18 18:09:51 +00:00
parent 611307a24b
commit fe3c32597d
57 changed files with 4689 additions and 655 deletions

View File

@@ -52,243 +52,27 @@
class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4"
style="perspective: 1200px;"
>
<!--
Card 1: 🚗 My Garage (Járműveim)
-->
<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="openCard('vehicles')"
>
<!-- 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>
<span class="ml-auto text-xs text-white/60">{{ vehicleStore.vehicles.length }} {{ t('dashboard.vehicles') }}</span>
</div>
<!-- Content -->
<div class="p-4 flex-1 flex flex-col text-slate-800 min-h-0">
<div class="flex-1 overflow-y-auto space-y-2">
<VehicleCardCompact
v-for="vehicle in displayVehicles.slice(0, 3)"
:key="vehicle.id"
:vehicle="vehicle"
@click="openCard('vehicles')"
@plate-click="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"
>
<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" />
</svg>
<p class="text-sm">{{ t('dashboard.noVehicles') }}</p>
</div>
</div>
</div>
</div>
<!-- Card 1: 🚗 My Garage (Járműveim) -->
<MyVehiclesCard
@open-card="openCard"
@open-vehicle-detail="openVehicleDetail"
/>
<!--
Card 2: 💰 Costs & Quick Actions (Action Center)
-->
<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 transition-all duration-300 ease-out transform hover:-translate-y-3 hover:scale-[1.02] hover:shadow-2xl"
>
<!-- Top header bar -->
<div class="h-12 bg-slate-700 w-full shrink-0 flex items-center px-4 gap-2">
<span class="text-white font-bold text-sm tracking-wide">💰 {{ t('dashboard.costsTitle') }}</span>
</div>
<!-- Content -->
<div class="p-4 flex-1 flex flex-col text-slate-800 overflow-hidden">
<!-- Vehicle Selector Dropdown -->
<div class="mb-3">
<label class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1 block">{{ t('dashboard.vehicle') }}</label>
<select
v-model="selectedActionVehicleId"
class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm text-slate-800 focus:border-sf-accent focus:ring-2 focus:ring-sf-accent/20 outline-none transition-all"
>
<option
v-for="v in vehicleStore.sortedVehicles"
:key="v.id"
:value="v.id"
>
{{ v.license_plate || '—' }} ({{ v.brand || '' }} {{ v.model || '' }})
</option>
</select>
</div>
<!-- Card 2: 💰 Costs & Quick Actions (Action Center) -->
<CostsActionsCard @cost-saved="onCostSaved" />
<!-- Action Buttons -->
<div class="flex-1 flex flex-col gap-2.5 justify-center">
<!-- No vehicle warning -->
<div
v-if="!selectedActionVehicle"
class="flex items-center justify-center rounded-xl bg-amber-50 border border-amber-200 px-4 py-3 text-sm text-amber-700"
>
<span> {{ t('dashboard.noVehicleSelected') }}</span>
</div>
<!-- Card 3: 🔧 Service Finder (3-Way Indítópult) -->
<ServiceFinderCard />
<!-- Record Fueling (Big, prominent) -->
<button
v-if="selectedActionVehicle"
@click="openFuelModal"
class="flex items-center gap-3 rounded-xl bg-gradient-to-r from-sf-accent to-emerald-500 px-4 py-3 text-sm font-bold text-white shadow-md transition-all hover:shadow-lg hover:scale-[1.02] active:scale-[0.98]"
>
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/20 text-lg"></span>
<span>{{ t('dashboard.recordFueling') }}</span>
<span class="ml-auto text-white/60 text-xs"></span>
</button>
<!-- Card 4: 🏆 Gamification (Játékosítás) -->
<GamificationCard
@open-card="openCard"
/>
<!-- 🅿 Parking / Toll (Medium) -->
<button
v-if="selectedActionVehicle"
@click="openFeeModal"
class="flex items-center gap-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-700 transition-all hover:bg-slate-100 hover:shadow-md active:scale-[0.98]"
>
<span class="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-200 text-base">🅿</span>
<span>{{ t('dashboard.parkingToll') }}</span>
<span class="ml-auto text-slate-400 text-xs"></span>
</button>
<!-- Additional Costs (Medium) -->
<button
v-if="selectedActionVehicle"
@click="openComplexModal"
class="flex items-center gap-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-700 transition-all hover:bg-slate-100 hover:shadow-md active:scale-[0.98]"
>
<span class="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-200 text-base"></span>
<span>{{ t('dashboard.additionalCosts') }}</span>
<span class="ml-auto text-slate-400 text-xs"></span>
</button>
</div>
</div>
</div>
<!--
Card 3: 🔧 Service Finder (3-Way Indítópult)
-->
<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 transition-all duration-300 ease-out transform hover:-translate-y-3 hover:scale-[1.02] hover:shadow-2xl cursor-pointer"
@click="openExternalServiceFinder"
>
<!-- 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('serviceFinder.title') }}</span>
</div>
<!-- Content: 3 simple buttons stacked vertically -->
<div class="p-4 flex-1 flex flex-col justify-center gap-2">
<button
@click.stop="openExternalServiceFinder"
class="w-full rounded-xl bg-sf-accent px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:bg-sf-accent/90 hover:shadow-md active:scale-[0.97] cursor-pointer"
>
🔍 {{ t('serviceFinder.plannedMaintenance') }}
</button>
<button
@click.stop="router.push('/dashboard/service-finder?mode=sos')"
class="w-full rounded-xl border border-red-300 bg-red-50 px-4 py-2.5 text-sm font-semibold text-red-700 shadow-sm transition-all hover:bg-red-100 hover:shadow-md active:scale-[0.97] cursor-pointer"
>
🚨 SOS {{ t('serviceFinder.sosTitle') }}
</button>
<button
@click.stop="isSfQuickAddOpen = true"
class="w-full rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-2.5 text-sm font-semibold text-slate-700 shadow-sm transition-all hover:border-sf-accent hover:bg-sf-accent/5 hover:shadow-md active:scale-[0.97] cursor-pointer"
>
{{ t('provider.quickAddTitle') }}
</button>
</div>
</div>
<!--
Card 4: 🏆 Gamification (Játékosítás)
-->
<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="openCard('gamification')"
>
<!-- 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.statsAndPoints') }}</span>
<span class="ml-auto inline-flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700">
{{ t('dashboard.live') }}
</span>
</div>
<!-- Content -->
<div class="p-4 flex-1 flex flex-col text-slate-800 overflow-hidden">
<div class="flex-1 space-y-3">
<!-- Score card -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-3 text-center">
<p class="text-3xl font-extrabold text-emerald-600">2,450</p>
<p class="text-xs text-slate-500 mt-1">{{ t('dashboard.monthlyScore') }}</p>
</div>
<!-- Achievement badges -->
<div>
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">
{{ t('dashboard.achievements') }}
</p>
<div class="flex flex-wrap gap-2">
<span
v-for="badge in badges"
:key="badge.label"
class="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs text-slate-600"
>
{{ badge.icon }} {{ badge.label }}
</span>
</div>
</div>
</div>
</div>
<!-- Bottom CTA button -->
<button class="mt-auto mb-4 mx-auto px-8 py-3 bg-slate-700 hover:bg-slate-800 text-white rounded-2xl font-medium text-sm transition-colors shadow-md">
{{ t('dashboard.refresh') }} 🏆
</button>
</div>
<!--
Card 5: 🛡 My Profile & Trust (Profil & Trust Score)
-->
<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="openCard('stats')"
>
<!-- 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('header.profile') }}</span>
</div>
<!-- Content -->
<div class="p-4 flex-1 flex flex-col text-slate-800 overflow-hidden">
<div class="flex-1 space-y-3">
<!-- User info -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-3">
<p class="text-sm font-bold text-slate-800">{{ authStore.userName || t('dashboard.guest') }}</p>
<p class="text-xs text-slate-400 mt-0.5">{{ authStore.user?.email }}</p>
</div>
<!-- Trust Score -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-3">
<div class="flex items-center justify-between mb-1">
<span class="text-xs text-slate-500 font-semibold uppercase tracking-wider">Trust Score</span>
<span class="text-sm font-bold text-emerald-600">A+</span>
</div>
<div class="h-2 overflow-hidden rounded-full bg-slate-200">
<div class="h-full rounded-full bg-gradient-to-r from-emerald-400 to-emerald-600 transition-all duration-500" style="width: 92%" />
</div>
</div>
<!-- Quick stats -->
<div class="grid grid-cols-2 gap-2">
<div class="rounded-lg border border-slate-200 bg-slate-50 p-2 text-center">
<p class="text-lg font-bold text-slate-800">{{ authStore.myOrganizations.length }}</p>
<p class="text-xs text-slate-400">{{ t('header.myCompanies') }}</p>
</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 p-2 text-center">
<p class="text-lg font-bold text-slate-800">{{ vehicleStore.vehicles.length }}</p>
<p class="text-xs text-slate-400">{{ t('dashboard.totalVehicles') }}</p>
</div>
</div>
</div>
</div>
<!-- Bottom CTA button -->
<button class="mt-auto mb-4 mx-auto px-8 py-3 bg-slate-700 hover:bg-slate-800 text-white rounded-2xl font-medium text-sm transition-colors shadow-md">
{{ t('header.profile') }}
</button>
</div>
<!-- Card 5: 🛡 My Profile & Trust (Profil & Trust Score) -->
<ProfileTrustCard
@open-card="openCard"
/>
</div>
</div>
</div>
@@ -337,36 +121,13 @@
</div>
</Teleport>
<!--
Cost Action Modals (Teleported to body)
-->
<SimpleFuelModal
:is-open="isFuelModalOpen"
:vehicle="selectedActionVehicle"
@close="isFuelModalOpen = false"
@saved="onModalSaved"
/>
<ComplexExpenseModal
:is-open="isFeeModalOpen"
:vehicle="selectedActionVehicle"
:is-fee-mode="true"
@close="isFeeModalOpen = false"
@saved="onModalSaved"
/>
<ComplexExpenseModal
:is-open="isComplexModalOpen"
:vehicle="selectedActionVehicle"
:is-fee-mode="false"
@close="isComplexModalOpen = false"
@saved="onModalSaved"
/>
<!--
Vehicle Detail Modal (Digital Twin 3-Tab Deep Link)
-->
<VehicleDetailModal
:is-open="isVehicleDetailOpen"
:vehicle="detailVehicle"
:refresh-trigger="refreshCostsTrigger"
@close="closeVehicleDetail"
@edit="handleEditFromDetail"
@set-primary="handleSetPrimaryFromDetail"
@@ -391,32 +152,29 @@
@saved="onEditVehicleSaved"
@deleted="onVehicleDeleted"
/>
<!--
Provider Quick Add Modal (Service Finder Card 3)
-->
<ProviderQuickAddModal
:is-open="isSfQuickAddOpen"
@close="isSfQuickAddOpen = false"
@saved="onSfQuickAddSaved"
/>
</div><!-- end min-h-screen -->
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { ref, onMounted, watch, nextTick } from 'vue'
import { useRouter, useRoute } 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 api from '../api/axios'
// ── Dashboard Card Components ──
import MyVehiclesCard from '../components/dashboard/MyVehiclesCard.vue'
import CostsActionsCard from '../components/dashboard/CostsActionsCard.vue'
import ServiceFinderCard from '../components/dashboard/ServiceFinderCard.vue'
import GamificationCard from '../components/dashboard/GamificationCard.vue'
import ProfileTrustCard from '../components/dashboard/ProfileTrustCard.vue'
// ── Shared Modals ──
import PrivateVehicleManager from '../components/dashboard/PrivateVehicleManager.vue'
import VehicleCardCompact from '../components/vehicle/VehicleCardCompact.vue'
import VehicleDetailModal from '../components/vehicle/VehicleDetailModal.vue'
import VehicleFormModal from '../components/dashboard/VehicleFormModal.vue'
import SimpleFuelModal from '../components/dashboard/SimpleFuelModal.vue'
import ComplexExpenseModal from '../components/dashboard/ComplexExpenseModal.vue'
import ProviderQuickAddModal from '../components/provider/ProviderQuickAddModal.vue'
const { t } = useI18n()
@@ -437,30 +195,16 @@ const openCard = (cardId: string, targetId: string | null = null) => {
selectedTargetId.value = targetId
}
// ── Service Finder (Card 3) — Launcher ──
const isSearchModalOpen = ref(false)
const isSfQuickAddOpen = ref(false)
/**
* Navigates to the Service Finder page in the same tab.
* Used by the card click and the "Tervezett karbantartás" button.
*/
function openExternalServiceFinder() {
router.push('/dashboard/service-finder')
}
/**
* Called when ProviderQuickAddModal saves a new provider.
*/
function onSfQuickAddSaved(provider: { id: number; name: string; category?: string | null; city?: string | null; address?: string | null }) {
isSfQuickAddOpen.value = false
alert(`🎉 ${t('provider.successMessage', { points: 0 })}`)
}
// ── Vehicle Detail Modal (Deep Link from plate click) ──
const isVehicleDetailOpen = ref(false)
const detailVehicle = ref<VehicleData | null>(null)
/**
* F5 Bug fix (RBAC Phase 3): Számláló, ami minden költség mentéskor nő.
* A VehicleDetailModal ezt figyelve újratölti a költségeket.
*/
const refreshCostsTrigger = ref(0)
function openVehicleDetail(vehicle: VehicleData) {
detailVehicle.value = vehicle
isVehicleDetailOpen.value = true
@@ -471,6 +215,15 @@ function closeVehicleDetail() {
detailVehicle.value = null
}
/**
* F5 Bug fix (RBAC Phase 3): Kezeli a CostsActionsCard cost-saved eseményét.
* Növeli a refreshCostsTrigger számlálót, ami a VehicleDetailModal-ban
* watch által figyelve újratölti a költségeket.
*/
function onCostSaved(vehicleId: string) {
refreshCostsTrigger.value++
}
// ── Vehicle Form Modal State (standalone, decoupled from Manager) ──
const isAddFormOpen = ref(false)
const isEditFormOpen = ref(false)
@@ -586,71 +339,6 @@ function handleSetPrimaryFromDetail(vehicle: VehicleData) {
vehicleStore.setPrimaryVehicle(vehicle.id)
}
/**
* Display vehicles from the backend API only.
* Sorted by is_primary flag (favorite first), then alphabetically.
* Shows all real vehicles; no mock/fallback data is injected.
*/
const displayVehicles = computed(() => {
return vehicleStore.sortedVehicles
})
// ── Cost Card Action Center State ──
const selectedActionVehicleId = ref<string>('')
const isFuelModalOpen = ref(false)
const isFeeModalOpen = ref(false)
const isComplexModalOpen = ref(false)
/** The currently selected vehicle object for the action modals */
const selectedActionVehicle = computed(() => {
if (!selectedActionVehicleId.value) return null
return vehicleStore.vehicles.find(v => v.id === selectedActionVehicleId.value) || null
})
/** Auto-select the first (primary/sorted) vehicle on load */
function autoSelectVehicle() {
if (vehicleStore.sortedVehicles.length > 0 && !selectedActionVehicleId.value) {
selectedActionVehicleId.value = vehicleStore.sortedVehicles[0].id
}
}
function openFuelModal() {
if (!selectedActionVehicle.value) return
isFuelModalOpen.value = true
}
function openFeeModal() {
if (!selectedActionVehicle.value) return
isFeeModalOpen.value = true
}
function openComplexModal() {
if (!selectedActionVehicle.value) return
isComplexModalOpen.value = true
}
function onModalSaved() {
isFuelModalOpen.value = false
isFeeModalOpen.value = false
isComplexModalOpen.value = false
}
// ── Placeholder notifications ──
const notifications = ref([
{ icon: '🔧', title: 'Service due for BMW X5', time: '2 hours ago' },
{ icon: '⛽', title: 'Fuel cost updated — +₿ 0.08', time: '5 hours ago' },
{ icon: '🏆', title: 'You earned "Eco Driver" badge!', time: '1 day ago' },
{ icon: '📊', title: 'Monthly report is ready', time: '2 days ago' },
])
// ── Placeholder badges ──
const badges = ref([
{ icon: '🌱', label: 'Eco Driver' },
{ icon: '📏', label: '10k km Club' },
{ icon: '🔧', label: 'Service Pro' },
{ icon: '⭐', label: 'Top Rater' },
])
onMounted(() => {
vehicleStore.fetchVehicles()
authStore.fetchMyOrganizations()
@@ -664,11 +352,6 @@ onMounted(() => {
}
})
// Watch for vehicles to load, then auto-select
watch(() => vehicleStore.vehicles.length, () => {
autoSelectVehicle()
}, { immediate: true })
// ── Watch for query param changes (hamburger menu navigation) ────
watch(() => route.query.card, (newCard) => {
if (newCard && typeof newCard === 'string') {