Files
service-finder/frontend/src/views/DashboardView.vue

379 lines
14 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="relative min-h-screen text-white">
<!-- Garage Background Image -->
<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>
<!-- Zen Mode FAB -->
<button
@click="isUiVisible = !isUiVisible"
class="fixed bottom-8 right-8 z-50 btn-premium rounded-full p-4 shadow-2xl shadow-black/40"
:title="isUiVisible ? t('zen.hide') : t('zen.show')"
>
<svg
v-if="isUiVisible"
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
<svg
v-else
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
</button>
<!-- Content with fade transition -->
<Transition name="fade">
<div v-if="isUiVisible" class="relative z-10">
<!-- Bottom-aligned 5-card container -->
<div class="flex flex-col justify-end min-h-[85vh] pb-8">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full">
<!-- 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>
<!-- 5-Card Grid -->
<div
v-if="!vehicleStore.isLoading"
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) -->
<MyVehiclesCard
@open-card="openCard"
@open-vehicle-detail="openVehicleDetail"
/>
<!-- Card 2: 💰 Costs & Quick Actions (Action Center) -->
<CostsActionsCard @cost-saved="onCostSaved" />
<!-- Card 3: 🔧 Service Finder (3-Way Indítópult) -->
<ServiceFinderCard />
<!-- Card 4: 🏆 Gamification (Játékosítás) -->
<GamificationCard
@open-card="openCard"
/>
<!-- Card 5: 🛡 My Profile & Trust (Profil & Trust Score) -->
<ProfileTrustCard
@open-card="openCard"
/>
</div>
</div>
</div>
</div><!-- end relative z-10 content wrapper -->
</Transition>
<!--
Flip Modal 3D átforduló részletes nézet (Teleport to body)
-->
<Teleport to="body">
<div
v-if="activeCard"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-slate-900/85 backdrop-blur-md"
@click.self="activeCard = null"
>
<div class="bg-white w-11/12 max-w-5xl h-auto min-h-[500px] pb-6 rounded-3xl shadow-2xl flex flex-col overflow-hidden animate-flip-in relative">
<!-- Close button (X) -->
<button
class="absolute top-4 right-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-slate-200/80 text-slate-600 hover:bg-slate-300 hover:text-slate-800 transition-colors text-xl font-bold"
@click="activeCard = null"
>
</button>
<!-- Dynamic content area -->
<div class="flex-1 p-8 overflow-y-auto text-slate-800">
<!-- Private Vehicle Manager -->
<PrivateVehicleManager
v-if="activeCard === 'vehicles'"
:target-vehicle-id="selectedTargetId"
@add-new="handleAddNew"
@edit-vehicle="handleEditVehicle"
/>
<!-- Fallback for other cards -->
<template v-if="activeCard && activeCard !== 'vehicles'">
<h2 class="text-2xl font-bold mb-6">
{{ t('dashboard.detailedView') }}: <span class="text-slate-500">{{ activeCard }}</span>
</h2>
<p class="text-slate-500">
{{ t('dashboard.modalPlaceholder') }}
</p>
</template>
</div>
</div>
</div>
</Teleport>
<!--
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"
/>
<!--
Vehicle Form Modal (Add) standalone, decoupled from Manager
-->
<VehicleFormModal
:is-open="isAddFormOpen"
@close="isAddFormOpen = false"
@saved="onAddVehicleSaved"
/>
<!--
Vehicle Form Modal (Edit) standalone, decoupled from Manager
-->
<VehicleFormModal
:is-open="isEditFormOpen"
:vehicle="editTargetVehicle"
@close="closeEditForm"
@saved="onEditVehicleSaved"
@deleted="onVehicleDeleted"
/>
</div><!-- end min-h-screen -->
</template>
<script setup lang="ts">
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 VehicleDetailModal from '../components/vehicle/VehicleDetailModal.vue'
import VehicleFormModal from '../components/dashboard/VehicleFormModal.vue'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
const vehicleStore = useVehicleStore()
// ── Zen Mode ──
const isUiVisible = ref(true)
// ── Flip Modal State ──
const activeCard = ref<string | null>(null)
const selectedTargetId = ref<string | null>(null)
const openCard = (cardId: string, targetId: string | null = null) => {
activeCard.value = cardId
selectedTargetId.value = targetId
}
// ── 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
}
function closeVehicleDetail() {
isVehicleDetailOpen.value = false
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)
const editTargetVehicle = ref<VehicleData | null>(null)
const isCheckingQuota = ref(false)
/**
* Return-State flag: when true, closing the edit form should re-open
* the VehicleDetailModal instead of returning to the dashboard.
*/
const returnToVehicleDetail = ref(false)
/**
* Preemptive quota check before opening the add-vehicle modal.
* Calls GET /api/v1/assets/vehicles/quota-status.
* If the user has reached their limit, shows a blocking toast/alert instead of opening the form.
*/
async function handleAddNew() {
if (isCheckingQuota.value) return
isCheckingQuota.value = true
try {
const res = await api.get('/assets/vehicles/quota-status')
const { can_add, current_count, limit } = res.data
if (!can_add) {
alert(
`Elérted a csomagodhoz tartozó járműlimitet (${current_count}/${limit})! ` +
'Kérjük, válts magasabb csomagra az új jármű rögzítéséhez.'
)
return
}
isAddFormOpen.value = true
} catch (err: any) {
console.error('[DashboardView] Quota check failed:', err)
isAddFormOpen.value = true
} finally {
isCheckingQuota.value = false
}
}
function onAddVehicleSaved() {
isAddFormOpen.value = false
}
function handleEditVehicle(vehicle: VehicleData) {
editTargetVehicle.value = vehicle ? { ...vehicle } : null
isEditFormOpen.value = true
}
function closeEditForm() {
isEditFormOpen.value = false
// If we came from the detail modal, return to it
if (returnToVehicleDetail.value) {
returnToVehicleDetail.value = false
const vehicle = editTargetVehicle.value
editTargetVehicle.value = null
if (vehicle) {
nextTick(() => {
detailVehicle.value = vehicle
isVehicleDetailOpen.value = true
})
}
} else {
editTargetVehicle.value = null
}
}
function onEditVehicleSaved(savedVehicle?: VehicleData) {
isEditFormOpen.value = false
// If we came from the detail modal, return to it
if (returnToVehicleDetail.value) {
returnToVehicleDetail.value = false
// Use the saved vehicle (with updated data) if available, otherwise fallback
const vehicle = savedVehicle || editTargetVehicle.value
editTargetVehicle.value = null
if (vehicle) {
nextTick(() => {
detailVehicle.value = vehicle
isVehicleDetailOpen.value = true
})
}
} else {
editTargetVehicle.value = null
}
}
/**
* Called when the edit VehicleFormModal emits 'deleted'.
* Completely resets the return-state and closes the edit form,
* then refreshes the vehicle list so the deleted vehicle disappears.
*/
function onVehicleDeleted() {
returnToVehicleDetail.value = false
isEditFormOpen.value = false
editTargetVehicle.value = null
vehicleStore.fetchVehicles()
}
// ── Bridge: Detail → Edit (opens edit modal directly, no Manager card) ──
function handleEditFromDetail(vehicle: VehicleData) {
returnToVehicleDetail.value = true
closeVehicleDetail()
// Use nextTick so detail modal closes before edit opens
nextTick(() => {
editTargetVehicle.value = vehicle ? { ...vehicle } : null
isEditFormOpen.value = true
})
}
function handleSetPrimaryFromDetail(vehicle: VehicleData) {
vehicleStore.setPrimaryVehicle(vehicle.id)
}
onMounted(() => {
vehicleStore.fetchVehicles()
authStore.fetchMyOrganizations()
// ── Handle query param card navigation ──────────────────────────
const cardParam = route.query.card as string | undefined
if (cardParam) {
openCard(cardParam)
// Clean up the query param so it doesn't re-trigger
router.replace({ query: {} })
}
})
// ── Watch for query param changes (hamburger menu navigation) ────
watch(() => route.query.card, (newCard) => {
if (newCard && typeof newCard === 'string') {
openCard(newCard)
router.replace({ query: {} })
}
})
</script>
<style scoped>
@keyframes flipIn {
from {
transform: perspective(1200px) rotateY(-90deg);
opacity: 0;
}
to {
transform: perspective(1200px) rotateY(0deg);
opacity: 1;
}
}
.animate-flip-in {
animation: flipIn 0.5s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;
}
</style>