Járműkezelés majdnem kész frontend

This commit is contained in:
Roo
2026-06-11 11:22:47 +00:00
parent 90e3173fbc
commit 0a3fd8de74
18 changed files with 2749 additions and 99 deletions

View File

@@ -0,0 +1,238 @@
<template>
<div class="flex flex-col h-full">
<!-- Header: Title + Counter + Add Button (nav arrows removed) -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-4">
<h2 class="text-2xl font-bold text-slate-800">Saját Járművek</h2>
<span
v-if="vehicles.length > 0"
class="inline-flex items-center rounded-full bg-sf-accent/10 px-3 py-1 text-xs font-semibold text-sf-accent"
>
{{ vehicles.length }} jármű
</span>
</div>
<div class="flex items-center gap-3">
<button
@click="isAddFormOpen = true"
class="inline-flex items-center gap-2 rounded-xl bg-sf-accent px-5 py-2.5 text-sm font-semibold text-white shadow-md transition-all hover:bg-sf-blue hover:shadow-lg"
>
<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="M12 4v16m8-8H4" />
</svg>
Új jármű hozzáadása
</button>
</div>
</div>
<!-- Carousel (with vehicles) or Empty State -->
<template v-if="vehicles.length > 0">
<div class="relative">
<!-- Left Edge Navigation Strip (only show when scrolling is needed) -->
<div
v-if="vehicles.length > 2"
@click="scrollLeft"
class="absolute left-0 top-0 bottom-0 w-12 bg-gradient-to-r from-white/80 to-transparent flex items-center justify-start cursor-pointer z-10 rounded-l-2xl"
>
<svg class="h-6 w-6 text-slate-500 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</div>
<!-- Right Edge Navigation Strip (only show when scrolling is needed) -->
<div
v-if="vehicles.length > 2"
@click="scrollRight"
class="absolute right-0 top-0 bottom-0 w-12 bg-gradient-to-l from-white/80 to-transparent flex items-center justify-end cursor-pointer z-10 rounded-r-2xl"
>
<svg class="h-6 w-6 text-slate-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
<div
ref="carouselRef"
class="flex overflow-x-auto snap-x snap-mandatory gap-6 pb-4 w-full scroll-smooth no-scrollbar"
:class="vehicles.length > 2 ? 'px-12' : 'px-2'"
style="scrollbar-width: none; -ms-overflow-style: none;"
>
<VehicleCardStandard
v-for="vehicle in vehicles"
:key="vehicle.id"
:vehicle="vehicle"
@click="openDetailView(vehicle)"
@edit="openEditDirect(vehicle)"
@set-primary="setPrimaryVehicle"
/>
</div>
</div>
</template>
<!-- Empty State (v-else) -->
<template v-else>
<div class="flex flex-1 flex-col items-center justify-center text-slate-400">
<svg class="mb-4 h-20 w-20 text-slate-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<h3 class="text-lg font-semibold text-slate-500">Még nincsenek járművek</h3>
<p class="mt-1 text-sm text-slate-400">Kattints az "Új jármű hozzáadása" gombra az első jármű felvételéhez.</p>
<button
@click="isAddFormOpen = true"
class="mt-6 inline-flex items-center gap-2 rounded-xl bg-sf-accent px-6 py-3 text-sm font-semibold text-white shadow-md transition-all hover:bg-sf-blue hover:shadow-lg"
>
<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="M12 4v16m8-8H4" />
</svg>
Új jármű hozzáadása
</button>
</div>
</template>
<!-- Vehicle Form Modal (Add) -->
<VehicleFormModal
:is-open="isAddFormOpen"
@close="isAddFormOpen = false"
@saved="onVehicleSaved"
/>
<!-- Vehicle Form Modal (Edit) -->
<VehicleFormModal
:is-open="isEditFormOpen"
:vehicle="editingVehicle"
@close="isEditFormOpen = false"
@saved="onVehicleEdited"
/>
<!-- Vehicle Detail Modal (read-only) -->
<VehicleDetailModal
:is-open="isDetailOpen"
:vehicle="detailVehicle"
@close="isDetailOpen = false"
@edit="openEditFromDetail"
@set-primary="setPrimaryVehicle"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, onMounted, watch } from 'vue'
import type { VehicleData } from '../../types/vehicle'
import { useVehicleStore } from '../../stores/vehicle'
import { mockVehicles } from '../../data/mockVehicles'
import VehicleFormModal from './VehicleFormModal.vue'
import VehicleDetailModal from '../vehicle/VehicleDetailModal.vue'
import VehicleCardStandard from '../vehicle/VehicleCardStandard.vue'
const props = defineProps<{
targetVehicleId?: string | null
}>()
const vehicleStore = useVehicleStore()
/**
* Smart vehicle list: real backend vehicles first, then mock data as fallback.
* Deduplicates by license_plate to avoid showing the same vehicle twice.
*/
const vehicles = computed<VehicleData[]>(() => {
const real = vehicleStore.vehicles as unknown as VehicleData[]
if (real.length >= 3) return real
const needed = 3 - real.length
const mocksToUse = mockVehicles
.filter(mv => !real.some(rv => (rv.license_plate || rv.licensePlate) === (mv.license_plate || mv.licensePlate)))
.slice(0, needed)
return [...real, ...mocksToUse]
})
// ── Carousel ref & scroll functions ──
const carouselRef = ref<HTMLElement | null>(null)
function scrollLeft() {
if (carouselRef.value) {
carouselRef.value.scrollBy({ left: -300, behavior: 'smooth' })
}
}
function scrollRight() {
if (carouselRef.value) {
carouselRef.value.scrollBy({ left: 300, behavior: 'smooth' })
}
}
// ── Detail Modal state ──
const isDetailOpen = ref(false)
const detailVehicle = ref<VehicleData | null>(null)
function openDetailView(vehicle: VehicleData) {
detailVehicle.value = vehicle
isDetailOpen.value = true
}
// ── Direct Edit (from card edit button) ──
function openEditDirect(vehicle: VehicleData) {
editingVehicle.value = vehicle ? { ...vehicle } : null
isEditFormOpen.value = true
}
// ── Bridge: Detail → Edit ──
async function openEditFromDetail(vehicle: VehicleData) {
isDetailOpen.value = false
// Use nextTick so detail modal closes before edit opens
await nextTick()
editingVehicle.value = vehicle ? { ...vehicle } : null
isEditFormOpen.value = true
}
// ── Modal state (Add) ──
const isAddFormOpen = ref(false)
function onVehicleSaved() {
isAddFormOpen.value = false
// In real implementation, refresh the vehicle list from the store
}
// ── Modal state (Edit) ──
const isEditFormOpen = ref(false)
const editingVehicle = ref<VehicleData | null>(null)
function onVehicleEdited() {
isEditFormOpen.value = false
editingVehicle.value = null
// In real implementation, refresh the vehicle list from the store
}
// ── Targeted scroll to vehicle card ──
function scrollToVehicle(licensePlate: string) {
const el = document.getElementById('vehicle-card-' + licensePlate)
if (el) {
el.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'center' })
}
}
onMounted(() => {
if (props.targetVehicleId) {
// Small delay to ensure DOM is rendered
setTimeout(() => scrollToVehicle(props.targetVehicleId!), 100)
}
})
watch(() => props.targetVehicleId, (newVal) => {
if (newVal) {
setTimeout(() => scrollToVehicle(newVal), 100)
}
})
// ── Set Primary Vehicle ──
async function setPrimaryVehicle(vehicle: VehicleData) {
console.log('[PrivateVehicleManager] setPrimaryVehicle called for:', vehicle.id, vehicle.license_plate)
// TODO: Real API call — PUT /api/v1/assets/vehicles/{id} with { is_primary: true }
// This will also need to unset any other primary vehicle for this user.
// For now, just log and update local state optimistically.
vehicleStore.setPrimaryVehicle(vehicle.id)
}
</script>
<style scoped>
/* Hide scrollbar for the carousel container */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
<template>
<div
class="flex items-center gap-3 p-2 hover:bg-slate-50 rounded-lg cursor-pointer transition-colors"
@click="$emit('click', vehicle)"
>
<!-- Mini EU License Plate (clickable) -->
<div
v-if="vehicle.license_plate"
@click.stop="$emit('plate-click', vehicle)"
>
<VehiclePlateBadge
:license-plate="vehicle.license_plate"
:country-code="vehicle.countryCode || vehicle.country_code"
size="sm"
/>
</div>
<!-- Nickname or Brand/Model text -->
<div class="flex-1 min-w-0">
<span class="text-sm font-semibold text-slate-700 truncate block">
{{ vehicle.nickname || vehicle.brand || vehicle.license_plate }}
</span>
<p v-if="vehicle.nickname && vehicle.brand" class="text-[11px] text-slate-400 truncate">
{{ vehicle.brand }}<span v-if="vehicle.model"> {{ vehicle.model }}</span>
</p>
</div>
</div>
</template>
<script setup lang="ts">
import type { VehicleData } from '../../types/vehicle'
import VehiclePlateBadge from './VehiclePlateBadge.vue'
defineProps<{
vehicle: VehicleData
}>()
defineEmits<{
click: [vehicle: VehicleData]
'plate-click': [vehicle: VehicleData]
}>()
</script>

View File

@@ -0,0 +1,170 @@
<template>
<div
:id="'vehicle-card-' + vehicle.license_plate"
class="group relative min-w-[260px] w-[280px] snap-center flex-shrink-0 rounded-2xl border border-slate-200 bg-white shadow-sm transition-all duration-200 hover:shadow-lg hover:-translate-y-1 overflow-hidden cursor-pointer"
@click="$emit('click', vehicle)"
>
<!-- Image Area (Top 40%) -->
<div class="h-36 rounded-t-2xl bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center overflow-hidden relative">
<template v-if="vehicle.image">
<img :src="vehicle.image" :alt="vehicle.brand" class="h-full w-full object-cover" />
</template>
<template v-else>
<!-- Car SVG silhouette placeholder -->
<div class="flex flex-col items-center justify-center text-slate-300">
<svg
class="w-16 h-16"
fill="none"
stroke="currentColor"
stroke-width="1.2"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19 17h2a1 1 0 001-1v-3.172a1 1 0 00-.293-.707l-2.828-2.828A1 1 0 0018.172 9H17l-3-3H7a2 2 0 00-2 2v2M5 17H3a1 1 0 01-1-1v-3.172a1 1 0 01.293-.707l2.828-2.828A1 1 0 016.828 9H7l3-3h5"
/>
<circle cx="7" cy="17" r="2" />
<circle cx="17" cy="17" r="2" />
</svg>
<span class="text-xs text-slate-400 mt-1">Nincs feltöltött fotó</span>
</div>
</template>
</div>
<!-- Card Body (relative for watermark) -->
<div class="p-4 space-y-3 relative overflow-hidden">
<!-- Halvány vízjel (kisebb, nem lóg ki) -->
<div
class="absolute inset-0 pointer-events-none select-none flex items-center justify-center overflow-hidden"
>
<span
class="text-5xl font-black text-slate-900/5 w-full text-center truncate px-4 leading-none"
>
{{ vehicle.brand }}
</span>
</div>
<!-- Nickname (if present) fixed height to prevent layout shift -->
<div class="min-h-[24px] mb-2">
<p
v-if="vehicle.nickname"
class="text-sm italic text-slate-500"
>
&bdquo;{{ vehicle.nickname }}&rdquo;
</p>
</div>
<!-- Dynamic EU License Plate Badge -->
<VehiclePlateBadge
:license-plate="vehicle.license_plate"
:country-code="vehicle.countryCode || vehicle.country_code"
size="md"
/>
<!-- Brand & Model -->
<h3 class="text-base font-bold text-slate-800 relative z-10">
{{ vehicle.brand }}
<span class="font-normal text-slate-500">{{ vehicle.model }}</span>
</h3>
<!-- Rich Data Rows (Icon + Text) -->
<div class="space-y-1.5 text-xs text-slate-500 relative z-10">
<!-- Year -->
<div class="flex items-center gap-2">
<svg class="h-3.5 w-3.5 shrink-0 text-slate-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>
<span>{{ vehicle.year_of_manufacture || vehicle.year }}</span>
</div>
<!-- Mileage -->
<div class="flex items-center gap-2">
<svg class="h-3.5 w-3.5 shrink-0 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
<span>{{ (vehicle.current_mileage ?? vehicle.mileage)?.toLocaleString() || '-' }} km</span>
</div>
<!-- Engine -->
<div class="flex items-center gap-2">
<svg class="h-3.5 w-3.5 shrink-0 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h-2m2 4h-2m-4-4h-2m2 4h-2m6-8h2a2 2 0 012 2v4a2 2 0 01-2 2h-2m-4 0H8a2 2 0 01-2-2V8a2 2 0 012-2h4m0 0V4m0 4v4" />
</svg>
<span>{{ vehicle.engine || vehicle.fuel_type || '—' }}</span>
</div>
<!-- Color -->
<div class="flex items-center gap-2">
<svg class="h-3.5 w-3.5 shrink-0 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
<span>{{ vehicle.individual_equipment?.color || vehicle.color || '—' }}</span>
</div>
<!-- Next Service (highlighted) -->
<div
class="flex items-center gap-2 pt-1"
:class="serviceDueClass"
>
<svg class="h-3.5 w-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="font-semibold">Következő Szerviz: {{ vehicle.nextService || 'Nincs adat' }}</span>
</div>
</div>
</div>
<!-- Primary Vehicle Star (top-left overlay) -->
<button
class="absolute top-3 left-3 flex h-8 w-8 items-center justify-center rounded-full shadow-sm backdrop-blur-sm transition-all z-20"
:class="vehicle.is_primary
? 'bg-amber-400 text-white hover:bg-amber-500'
: 'bg-white/60 text-slate-300 opacity-0 group-hover:opacity-100 hover:text-amber-400 hover:bg-white/90'"
@click.stop="$emit('set-primary', vehicle)"
:title="vehicle.is_primary ? 'Elsődleges jármű' : 'Beállítás elsődlegessé'"
>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
</button>
<!-- Edit Button (top-right overlay) -->
<button
class="absolute top-3 right-3 flex h-8 w-8 items-center justify-center rounded-full bg-white/80 text-slate-400 opacity-0 shadow-sm backdrop-blur-sm transition-all hover:bg-sf-accent hover:text-white group-hover:opacity-100 z-20"
@click.stop="$emit('edit', vehicle)"
>
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { VehicleData } from '../../types/vehicle'
import VehiclePlateBadge from './VehiclePlateBadge.vue'
const props = defineProps<{
vehicle: VehicleData
}>()
defineEmits<{
click: [vehicle: VehicleData]
edit: [vehicle: VehicleData]
'set-primary': [vehicle: VehicleData]
}>()
const serviceDueClass = computed(() => {
if (!props.vehicle.nextService) return 'text-slate-500'
const match = props.vehicle.nextService.match(/([\d\s]+)/)
if (!match) return 'text-slate-500'
const kmStr = match[1].replace(/\s/g, '')
const km = parseInt(kmStr, 10)
if (isNaN(km)) return 'text-slate-500'
if (km > 10000) return 'text-green-600'
if (km > 3000) return 'text-orange-500'
return 'text-red-600'
})
</script>

View File

@@ -0,0 +1,183 @@
<template>
<Teleport to="body">
<div
v-if="isOpen"
class="fixed inset-0 z-[10000] flex items-center justify-center bg-black/50 backdrop-blur-sm"
@click.self="$emit('close')"
>
<div
@click.stop
class="relative w-full max-w-2xl mx-4 rounded-2xl border border-slate-200 bg-white shadow-2xl overflow-hidden animate-scale-in"
>
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white">
<div class="flex items-center gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-sf-accent/10">
<svg class="h-5 w-5 text-sf-accent" 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>
<div class="flex items-center gap-2">
<h3 class="text-lg font-bold text-slate-800">{{ vehicle?.brand }} {{ vehicle?.model }}</h3>
<!-- Primary Vehicle Star -->
<button
v-if="vehicle"
@click.stop="$emit('set-primary', vehicle)"
class="flex h-7 w-7 items-center justify-center rounded-full transition-all cursor-pointer"
:class="vehicle.is_primary
? 'bg-amber-100 text-amber-500 hover:bg-amber-200'
: 'bg-slate-100 text-slate-300 hover:bg-amber-50 hover:text-amber-400'"
:title="vehicle.is_primary ? 'Elsődleges jármű' : 'Beállítás elsődlegessé'"
>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
</button>
</div>
<p class="text-xs text-slate-400">{{ vehicle?.nickname ? `"${vehicle.nickname}"` : '' }}</p>
</div>
</div>
<button
@click="$emit('close')"
class="flex h-8 w-8 items-center justify-center rounded-full text-slate-400 transition-colors duration-200 hover:bg-red-50 hover:text-red-500 cursor-pointer"
aria-label="Bezárás"
>
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
<!-- Body -->
<div class="p-6 max-h-[65vh] overflow-y-auto">
<!-- EU License Plate Hero -->
<div class="flex justify-center mb-6">
<VehiclePlateBadge
:license-plate="vehicle?.license_plate || ''"
:country-code="vehicle?.countryCode || vehicle?.country_code"
size="lg"
/>
</div>
<!-- Info Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Brand -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Márka</p>
<p class="text-sm font-bold text-slate-800">{{ vehicle?.brand || '—' }}</p>
</div>
<!-- Model -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Modell</p>
<p class="text-sm font-bold text-slate-800">{{ vehicle?.model || '—' }}</p>
</div>
<!-- Year -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Évjárat</p>
<p class="text-sm font-bold text-slate-800">{{ vehicle?.year || vehicle?.year_of_manufacture || '—' }}</p>
</div>
<!-- Mileage -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Km óra állás</p>
<p class="text-sm font-bold text-slate-800">{{ (vehicle?.current_mileage ?? vehicle?.mileage)?.toLocaleString() || '—' }} km</p>
</div>
<!-- Engine -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Motor</p>
<p class="text-sm font-bold text-slate-800">{{ vehicle?.engine || '—' }}</p>
</div>
<!-- Color -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Szín</p>
<div class="flex items-center gap-2">
<span class="text-sm font-bold text-slate-800">{{ vehicle?.individual_equipment?.color || vehicle?.color || '—' }}</span>
<span
v-if="vehicle?.individual_equipment?.color || vehicle?.color"
class="inline-block h-4 w-4 rounded-full border border-slate-300"
:style="{ backgroundColor: vehicle?.individual_equipment?.color || vehicle?.color }"
></span>
</div>
</div>
<!-- Next Service -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4 sm:col-span-2">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Következő szerviz</p>
<p class="text-sm font-bold" :class="serviceDueClass">{{ vehicle?.nextService || '—' }}</p>
</div>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-end gap-3 border-t border-slate-200 px-6 py-4 bg-slate-50">
<button
@click="$emit('close')"
class="inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-5 py-2.5 text-sm font-semibold text-slate-600 transition-all hover:bg-slate-100 cursor-pointer"
>
Bezárás
</button>
<button
@click="$emit('edit', vehicle)"
class="inline-flex items-center gap-2 rounded-xl bg-sf-accent px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:bg-sf-blue cursor-pointer"
>
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Szerkesztés
</button>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { VehicleData } from '../../types/vehicle'
import VehiclePlateBadge from './VehiclePlateBadge.vue'
const props = defineProps<{
isOpen: boolean
vehicle: VehicleData | null
}>()
const emit = defineEmits<{
close: []
edit: [vehicle: VehicleData]
'set-primary': [vehicle: VehicleData]
}>()
const serviceDueClass = computed(() => {
if (!props.vehicle?.nextService) return 'text-slate-500'
const match = props.vehicle.nextService.match(/([\d\s]+)/)
if (!match) return 'text-slate-500'
const kmStr = match[1].replace(/\s/g, '')
const km = parseInt(kmStr, 10)
if (isNaN(km)) return 'text-slate-500'
if (km > 10000) return 'text-green-600'
if (km > 3000) return 'text-orange-500'
return 'text-red-600'
})
</script>
<style scoped>
@keyframes scaleIn {
from {
transform: scale(0.9);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.animate-scale-in {
animation: scaleIn 0.2s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<div
class="inline-flex items-center overflow-hidden rounded border border-gray-400 shadow-sm shrink-0 transition-transform duration-200 hover:-translate-y-0.5"
:class="containerClasses"
>
<!-- EU blue band -->
<div
class="flex flex-col items-center justify-center bg-[#003399] text-white"
:class="bandClasses"
>
<div class="flex flex-wrap justify-center gap-[1px] px-0.5">
<span
v-for="i in 12"
:key="i"
class="inline-block rounded-full bg-yellow-300"
:class="starClasses"
></span>
</div>
<span class="font-black leading-none tracking-wide" :class="codeClasses">
{{ countryCode || 'H' }}
</span>
</div>
<!-- Plate number -->
<div
class="flex items-center justify-center bg-white font-mono font-bold tracking-widest text-gray-900"
:class="plateClasses"
>
{{ licensePlate }}
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(defineProps<{
licensePlate: string
countryCode?: string
size?: 'sm' | 'md' | 'lg'
}>(), {
countryCode: 'H',
size: 'md',
})
const containerClasses = computed(() => {
switch (props.size) {
case 'sm': return 'rounded'
case 'lg': return 'rounded-lg'
default: return 'rounded-md'
}
})
const bandClasses = computed(() => {
switch (props.size) {
case 'sm': return 'w-5 h-6 py-0.5'
case 'lg': return 'w-10 h-14 py-1'
default: return 'w-8 h-10 py-0.5'
}
})
const starClasses = computed(() => {
switch (props.size) {
case 'sm': return 'h-[2px] w-[2px]'
case 'lg': return 'h-[3px] w-[3px]'
default: return 'h-[2px] w-[2px]'
}
})
const codeClasses = computed(() => {
switch (props.size) {
case 'sm': return 'text-[6px]'
case 'lg': return 'text-xs'
default: return 'text-[10px]'
}
})
const plateClasses = computed(() => {
switch (props.size) {
case 'sm': return 'h-6 px-1.5 text-[10px]'
case 'lg': return 'h-14 px-2 text-base'
default: return 'h-10 px-1 text-sm'
}
})
</script>