egyedi jármű szerkesztés előtti mentés

This commit is contained in:
Roo
2026-06-12 07:56:15 +00:00
parent 0a3fd8de74
commit ef8df9608c
29 changed files with 3863 additions and 396 deletions

View File

@@ -0,0 +1,311 @@
<template>
<Teleport to="body">
<div
v-if="isOpen"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-slate-900/60 backdrop-blur-sm"
@click.self="$emit('close')"
>
<div class="bg-white w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden animate-fade-in-up">
<!-- Header -->
<div class="flex items-center justify-between bg-slate-700 px-6 py-4">
<h3 class="text-lg font-bold text-white flex items-center gap-2">
<span>{{ isFeeMode ? '🅿️' : '' }}</span>
{{ isFeeMode ? 'Parkolás / Útdíj rögzítése' : 'További költség rögzítése' }}
</h3>
<button
class="flex h-8 w-8 items-center justify-center rounded-full bg-white/10 text-white/70 hover:bg-white/20 hover:text-white transition-colors"
@click="$emit('close')"
>
</button>
</div>
<!-- Form Body -->
<form @submit.prevent="handleSubmit" class="p-6 space-y-5">
<!-- Vehicle selector (read-only display) -->
<div class="rounded-lg bg-slate-50 border border-slate-200 px-4 py-3 text-sm text-slate-600">
<span class="font-semibold text-slate-800">{{ vehicleLabel }}</span>
</div>
<!-- Date -->
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1.5">Dátum</label>
<input
v-model="form.date"
type="date"
required
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:ring-2 focus:ring-sf-accent/20 outline-none transition-all"
/>
</div>
<!-- Amount (HUF) -->
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1.5">Összeg (HUF)</label>
<input
v-model.number="form.amount"
type="number"
min="0"
step="1"
required
placeholder="pl. 5000"
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:ring-2 focus:ring-sf-accent/20 outline-none transition-all"
/>
</div>
<!-- Cascade Category: Main Category -->
<div v-if="!isFeeMode">
<label class="block text-sm font-semibold text-slate-700 mb-1.5">Főkategória</label>
<select
v-model="form.mainCategory"
required
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:ring-2 focus:ring-sf-accent/20 outline-none transition-all"
@change="onMainCategoryChange"
>
<option value="" disabled>Válassz kategóriát...</option>
<option
v-for="cat in mainCategories"
:key="cat.id"
:value="cat.id"
>
{{ cat.name }}
</option>
</select>
</div>
<!-- Cascade Category: Sub Category -->
<div v-if="!isFeeMode && subCategories.length > 0">
<label class="block text-sm font-semibold text-slate-700 mb-1.5">Alkategória</label>
<select
v-model="form.subCategory"
required
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:ring-2 focus:ring-sf-accent/20 outline-none transition-all"
>
<option value="" disabled>Válassz alkategóriát...</option>
<option
v-for="sub in subCategories"
:key="sub.id"
:value="sub.id"
>
{{ sub.name }}
</option>
</select>
</div>
<!-- Description / Notes -->
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1.5">Megjegyzés</label>
<textarea
v-model="form.description"
rows="3"
placeholder="Rövid leírás a költségről..."
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:ring-2 focus:ring-sf-accent/20 outline-none transition-all resize-none"
/>
</div>
<!-- Recurring Toggle -->
<div v-if="!isFeeMode" class="flex items-center gap-3">
<input
id="is-recurring"
v-model="form.isRecurring"
type="checkbox"
class="h-4 w-4 rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20"
/>
<label for="is-recurring" class="text-sm text-slate-700">Rendszeres költség (havi ismétlődő)</label>
</div>
<!-- Error message -->
<div
v-if="errorMessage"
class="rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700"
>
{{ errorMessage }}
</div>
<!-- Submit button -->
<button
type="submit"
:disabled="costStore.isSubmitting"
class="w-full rounded-xl bg-sf-accent py-3 text-sm font-bold text-white shadow-md transition-all hover:bg-sf-blue hover:shadow-lg disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<svg
v-if="costStore.isSubmitting"
class="h-4 w-4 animate-spin"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span v-else> Költség rögzítése</span>
</button>
</form>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useCostStore } from '../../stores/cost'
import api from '../../api/axios'
import type { Vehicle } from '../../stores/vehicle'
const props = defineProps<{
isOpen: boolean
vehicle: Vehicle | null
/** If true, pre-fills as FEES category (parking/toll) */
isFeeMode?: boolean
}>()
const emit = defineEmits<{
close: []
saved: []
}>()
const costStore = useCostStore()
// ── Form State ──
const form = reactive({
date: new Date().toISOString().slice(0, 10),
amount: 0,
mainCategory: '',
subCategory: '',
description: '',
isRecurring: false,
})
const errorMessage = ref<string | null>(null)
// ── Category Cascade State ──
interface CategoryOption {
id: number
name: string
parent_id: number | null
}
const mainCategories = ref<CategoryOption[]>([])
const subCategories = ref<CategoryOption[]>([])
const vehicleLabel = computed(() => {
if (!props.vehicle) return 'Nincs kiválasztva jármű'
const plate = props.vehicle.license_plate || ''
const brand = props.vehicle.brand || ''
const model = props.vehicle.model || ''
return `${plate} (${brand} ${model})`.trim()
})
/**
* Fetch hierarchical categories from the dictionaries API.
*/
async function fetchCategories() {
try {
const res = await api.get('/dictionaries/cost-categories')
const allCategories = res.data as CategoryOption[]
// Split into main (parent_id === null) and sub categories
mainCategories.value = allCategories.filter((c) => c.parent_id === null)
// Store all for sub-category filtering
window.__allCostCategories = allCategories
} catch (err) {
console.error('[ComplexExpenseModal] Failed to load categories:', err)
// Fallback: provide basic categories
mainCategories.value = [
{ id: 1, name: 'Üzemanyag', parent_id: null },
{ id: 2, name: 'Szerviz / Karbantartás', parent_id: null },
{ id: 3, name: 'Parkolás / Útdíj', parent_id: null },
{ id: 4, name: 'Biztosítás', parent_id: null },
{ id: 5, name: 'Adók / Illetékek', parent_id: null },
{ id: 6, name: 'Egyéb', parent_id: null },
]
}
}
function onMainCategoryChange() {
// Reset sub-category when main changes
form.subCategory = ''
subCategories.value = []
if (!form.mainCategory) return
const allCats: CategoryOption[] = (window as any).__allCostCategories || []
subCategories.value = allCats.filter(
(c) => c.parent_id === Number(form.mainCategory)
)
}
async function handleSubmit() {
if (!props.vehicle) {
errorMessage.value = 'Nincs kiválasztva jármű.'
return
}
errorMessage.value = null
// Determine cost_type and category_id
let costType: string
let categoryId: number
if (props.isFeeMode) {
// Fee mode: pre-filled as FEES
costType = 'fee'
categoryId = 3 // Parkolás / Útdíj
} else {
// Complex mode: use selected sub-category or main category
costType = 'other'
categoryId = Number(form.subCategory || form.mainCategory) || 6
}
const payload = {
asset_id: props.vehicle.id,
// organization_id is omitted — backend auto-resolves from asset
category_id: categoryId,
cost_type: costType,
amount_local: form.amount,
currency_local: 'HUF' as const,
date: new Date(form.date).toISOString(),
mileage_at_cost: null,
description: form.description || (props.isFeeMode ? 'Parkolás / Útdíj' : 'Egyéb költség'),
data: {
is_recurring: form.isRecurring,
category_id: categoryId,
main_category_id: form.mainCategory || null,
sub_category_id: form.subCategory || null,
},
}
const result = await costStore.addExpense(payload)
if (result) {
// Reset form
form.date = new Date().toISOString().slice(0, 10)
form.amount = 0
form.mainCategory = ''
form.subCategory = ''
form.description = ''
form.isRecurring = false
emit('saved')
} else {
errorMessage.value = costStore.lastError || 'Ismeretlen hiba történt.'
}
}
onMounted(() => {
fetchCategories()
})
</script>
<style scoped>
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.25s ease-out forwards;
}
</style>

View File

@@ -13,13 +13,23 @@
</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"
@click="handleAddNewClick"
:disabled="isCheckingQuota"
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 disabled:opacity-60 disabled:cursor-not-allowed"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg
v-if="isCheckingQuota"
class="h-4 w-4 animate-spin"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<svg v-else 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
{{ isCheckingQuota ? 'Ellenőrzés...' : 'Új jármű hozzáadása' }}
</button>
</div>
</div>
@@ -76,13 +86,23 @@
<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"
@click="handleAddNewClick"
:disabled="isCheckingQuota"
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 disabled:opacity-60 disabled:cursor-not-allowed"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg
v-if="isCheckingQuota"
class="h-4 w-4 animate-spin"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<svg v-else 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
{{ isCheckingQuota ? 'Ellenőrzés...' : 'Új jármű hozzáadása' }}
</button>
</div>
</template>
@@ -109,6 +129,8 @@
@edit="openEditFromDetail"
@set-primary="setPrimaryVehicle"
/>
<!-- Task 3: Hidden trigger for direct edit from DashboardView -->
<div style="display: none">{{ triggerEditFromParent }}</div>
</div>
</template>
@@ -116,30 +138,30 @@
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 api from '../../api/axios'
import VehicleFormModal from './VehicleFormModal.vue'
import VehicleDetailModal from '../vehicle/VehicleDetailModal.vue'
import VehicleCardStandard from '../vehicle/VehicleCardStandard.vue'
const props = defineProps<{
targetVehicleId?: string | null
editTargetVehicle?: VehicleData | null
}>()
const emit = defineEmits<{
'clear-edit-target': []
}>()
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.
* Vehicle list sourced exclusively from the backend API.
* Sorted by is_primary flag (favorite first), then alphabetically.
* No mock/fallback data is injected — if the API returns an empty list,
* the UI displays an empty garage.
*/
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]
return vehicleStore.sortedVehicles as unknown as VehicleData[]
})
// ── Carousel ref & scroll functions ──
@@ -183,6 +205,40 @@ async function openEditFromDetail(vehicle: VehicleData) {
// ── Modal state (Add) ──
const isAddFormOpen = ref(false)
const isCheckingQuota = 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 handleAddNewClick() {
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) {
// Block — show a prominent warning instead of opening the modal
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
}
// Quota OK — open the add form
isAddFormOpen.value = true
} catch (err: any) {
console.error('[PrivateVehicleManager] Quota check failed:', err)
// Fail open — if the API is unreachable, let the user proceed
isAddFormOpen.value = true
} finally {
isCheckingQuota.value = false
}
}
function onVehicleSaved() {
isAddFormOpen.value = false
@@ -220,6 +276,18 @@ watch(() => props.targetVehicleId, (newVal) => {
}
})
// ── Task 3: Watch for editTargetVehicle from parent (DashboardView) ──
const triggerEditFromParent = computed(() => {
if (props.editTargetVehicle) {
// Open the edit form directly with the vehicle data
editingVehicle.value = { ...props.editTargetVehicle } as VehicleData
isEditFormOpen.value = true
// Clear the prop so it doesn't re-trigger
emit('clear-edit-target')
}
return props.editTargetVehicle?.id || null
})
// ── Set Primary Vehicle ──
async function setPrimaryVehicle(vehicle: VehicleData) {
console.log('[PrivateVehicleManager] setPrimaryVehicle called for:', vehicle.id, vehicle.license_plate)

View File

@@ -0,0 +1,200 @@
<template>
<Teleport to="body">
<div
v-if="isOpen"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-slate-900/60 backdrop-blur-sm"
@click.self="$emit('close')"
>
<div class="bg-white w-full max-w-md rounded-2xl shadow-2xl overflow-hidden animate-fade-in-up">
<!-- Header -->
<div class="flex items-center justify-between bg-slate-700 px-6 py-4">
<h3 class="text-lg font-bold text-white flex items-center gap-2">
<span></span> Tankolás rögzítése
</h3>
<button
class="flex h-8 w-8 items-center justify-center rounded-full bg-white/10 text-white/70 hover:bg-white/20 hover:text-white transition-colors"
@click="$emit('close')"
>
</button>
</div>
<!-- Form Body -->
<form @submit.prevent="handleSubmit" class="p-6 space-y-5">
<!-- Vehicle selector (read-only display of selected vehicle) -->
<div class="rounded-lg bg-slate-50 border border-slate-200 px-4 py-3 text-sm text-slate-600">
<span class="font-semibold text-slate-800">{{ vehicleLabel }}</span>
</div>
<!-- Date -->
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1.5">Dátum</label>
<input
v-model="form.date"
type="date"
required
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:ring-2 focus:ring-sf-accent/20 outline-none transition-all"
/>
</div>
<!-- Amount (HUF) -->
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1.5">Összeg (HUF)</label>
<input
v-model.number="form.amount"
type="number"
min="0"
step="1"
required
placeholder="pl. 15000"
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:ring-2 focus:ring-sf-accent/20 outline-none transition-all"
/>
</div>
<!-- Quantity (Liters/kWh) -->
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1.5">Mennyiség (Liter / kWh)</label>
<input
v-model.number="form.quantity"
type="number"
min="0"
step="0.1"
required
placeholder="pl. 45.5"
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:ring-2 focus:ring-sf-accent/20 outline-none transition-all"
/>
</div>
<!-- Odometer -->
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1.5">Km óra állás</label>
<input
v-model.number="form.odometer"
type="number"
min="0"
step="1"
placeholder="pl. 123456"
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:ring-2 focus:ring-sf-accent/20 outline-none transition-all"
/>
</div>
<!-- Error message -->
<div
v-if="errorMessage"
class="rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700"
>
{{ errorMessage }}
</div>
<!-- Submit button -->
<button
type="submit"
:disabled="costStore.isSubmitting"
class="w-full rounded-xl bg-sf-accent py-3 text-sm font-bold text-white shadow-md transition-all hover:bg-sf-blue hover:shadow-lg disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<svg
v-if="costStore.isSubmitting"
class="h-4 w-4 animate-spin"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span v-else> Tankolás rögzítése</span>
</button>
</form>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { useCostStore } from '../../stores/cost'
import type { Vehicle } from '../../stores/vehicle'
const props = defineProps<{
isOpen: boolean
vehicle: Vehicle | null
}>()
const emit = defineEmits<{
close: []
saved: []
}>()
const costStore = useCostStore()
const form = reactive({
date: new Date().toISOString().slice(0, 10),
amount: 0,
quantity: 0,
odometer: 0,
})
const errorMessage = ref<string | null>(null)
const vehicleLabel = computed(() => {
if (!props.vehicle) return 'Nincs kiválasztva jármű'
const plate = props.vehicle.license_plate || ''
const brand = props.vehicle.brand || ''
const model = props.vehicle.model || ''
return `${plate} (${brand} ${model})`.trim()
})
async function handleSubmit() {
if (!props.vehicle) {
errorMessage.value = 'Nincs kiválasztva jármű.'
return
}
errorMessage.value = null
// Build payload: cost_type = 'fuel', category auto-set to FUEL
// organization_id is omitted — backend auto-resolves from asset
const payload = {
asset_id: props.vehicle.id,
category_id: 1, // FUEL category (default)
cost_type: 'fuel',
amount_local: form.amount,
currency_local: 'HUF' as const,
date: new Date(form.date).toISOString(),
mileage_at_cost: form.odometer > 0 ? form.odometer : null,
description: `Tankolás: ${form.quantity} L/kWh`,
data: {
quantity: form.quantity,
fuel_type: props.vehicle.fuel_type || null,
},
}
const result = await costStore.addExpense(payload)
if (result) {
// Reset form
form.date = new Date().toISOString().slice(0, 10)
form.amount = 0
form.quantity = 0
form.odometer = 0
emit('saved')
} else {
errorMessage.value = costStore.lastError || 'Ismeretlen hiba történt.'
}
}
</script>
<style scoped>
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.25s ease-out forwards;
}
</style>

View File

@@ -3,11 +3,11 @@
<div
v-if="isOpen"
class="fixed inset-0 z-[10001] 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 flex flex-col"
:class="{ 'animate-spin-shrink-out': isClosing }"
style="max-height: 90vh;"
>
<!-- Modal Header -->
@@ -22,7 +22,7 @@
<h3 class="text-lg font-bold text-slate-800">{{ vehicle ? 'Jármű szerkesztése' : 'Új jármű hozzáadása' }}</h3>
</div>
<button
@click="$emit('close')"
@click="handleClose"
class="text-slate-400 transition-colors duration-200 hover:text-red-500 cursor-pointer"
aria-label="Bezárás"
>
@@ -43,8 +43,9 @@
<!-- Step circle -->
<button
@click="goToStep(idx)"
class="flex items-center justify-center w-8 h-8 rounded-full text-xs font-bold transition-all duration-200 cursor-pointer"
class="flex items-center justify-center w-8 h-8 rounded-full text-xs font-bold transition-all duration-200"
:class="getStepCircleClass(idx)"
:disabled="isEditMode && idx === 0"
>
<span v-if="step > idx"></span>
<span v-else>{{ idx + 1 }}</span>
@@ -75,12 +76,16 @@
<button
v-for="vc in vehicleClasses"
:key="vc.value"
@click="form.vehicle_class = vc.value"
@click="!isEditMode && (form.vehicle_class = vc.value)"
type="button"
class="flex flex-col items-center gap-1 rounded-xl border-2 px-2 py-2 text-xs font-medium transition-all duration-200 cursor-pointer"
:class="form.vehicle_class === vc.value
? 'border-sf-accent bg-sf-accent/5 text-sf-accent shadow-sm'
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50'"
:disabled="isEditMode"
class="flex flex-col items-center gap-1 rounded-xl border-2 px-2 py-2 text-xs font-medium transition-all duration-200"
:class="[
form.vehicle_class === vc.value
? 'border-sf-accent bg-sf-accent/5 text-sf-accent shadow-sm'
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50',
isEditMode ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
]"
>
<span class="text-lg">{{ vc.icon }}</span>
<span>{{ vc.label }}</span>
@@ -130,10 +135,98 @@
</div>
</div>
<!-- -->
<!-- ARCHIVE CONFIRMATION (shown instead of steps when active) -->
<!-- -->
<div v-if="showArchiveConfirm" class="space-y-6">
<div class="rounded-xl border border-red-200 bg-red-50 p-6">
<div class="flex items-center gap-3 mb-4">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<svg class="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<div>
<h3 class="text-lg font-bold text-red-800">Jármű végleges eltávolítása</h3>
<p class="text-sm text-red-600">Ez a művelet nem vonható vissza! A jármű archiválásra kerül.</p>
</div>
</div>
<!-- License Plate Confirmation -->
<div class="mb-4">
<label class="mb-1.5 block text-sm font-medium text-red-700">
A megerősítéshez gépeld be a rendszámot: <strong>{{ vehicle?.license_plate }}</strong>
</label>
<input
v-model="archiveConfirmPlate"
type="text"
class="w-full rounded-xl border border-red-300 bg-white px-4 py-2.5 text-sm text-slate-800 placeholder-slate-400 focus:border-red-500 focus:outline-none focus:ring-2 focus:ring-red-500/20 uppercase"
:placeholder="vehicle?.license_plate || 'Rendszám'"
/>
</div>
<!-- Final Mileage -->
<div class="mb-4">
<label class="mb-1.5 block text-sm font-medium text-red-700">
Utolsó km óra állás
</label>
<input
v-model.number="archiveFinalMileage"
type="number"
min="0"
class="w-full rounded-xl border border-red-300 bg-white px-4 py-2.5 text-sm text-slate-800 placeholder-slate-400 focus:border-red-500 focus:outline-none focus:ring-2 focus:ring-red-500/20"
placeholder="0"
/>
</div>
<!-- Archive Reason -->
<div class="mb-4">
<label class="mb-1.5 block text-sm font-medium text-red-700">
Eltávolítás oka <span class="text-red-500">*</span>
</label>
<select
v-model="archiveReason"
class="w-full rounded-xl border border-red-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-red-500 focus:outline-none focus:ring-2 focus:ring-red-500/20 appearance-none cursor-pointer"
>
<option value="" disabled>Válassz okot</option>
<option value="Eladás">Eladás</option>
<option value="Gazdasági totálkár / Bontás">Gazdasági totálkár / Bontás</option>
<option value="Lízing/Bérlet lejárta">Lízing/Bérlet lejárta</option>
<option value="Téves rögzítés">Téves rögzítés</option>
<option value="Egyéb">Egyéb</option>
</select>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-3">
<button
@click="cancelArchive"
class="inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-600 transition-all hover:bg-slate-100 cursor-pointer"
>
Mégsem
</button>
<button
@click="confirmArchive"
:disabled="!isArchiveValid || isArchiving"
class="inline-flex items-center gap-2 rounded-xl bg-red-600 px-5 py-2 text-sm font-semibold text-white shadow-sm transition-all hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
>
<svg v-if="isArchiving" class="h-4 w-4 animate-spin" 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>
<svg v-else 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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
{{ isArchiving ? 'Eltávolítás...' : 'Végleges törlés' }}
</button>
</div>
</div>
</div>
<!-- -->
<!-- STEP 1: ALAPADATOK -->
<!-- -->
<div v-if="step === 1" class="space-y-4">
<div v-if="step === 1 && !showArchiveConfirm" class="space-y-4">
<!-- Edit Mode: License Plate & VIN (only shown when editing) -->
<div v-if="isEditMode" class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
@@ -276,6 +369,26 @@
placeholder="0"
/>
</div>
<!-- DANGER ZONE (Edit Mode only) -->
<div v-if="isEditMode" class="mt-8 rounded-xl border-2 border-red-200 bg-red-50/50 p-5">
<div class="flex items-center gap-2 mb-3">
<svg class="h-5 w-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<h4 class="text-sm font-bold text-red-700 uppercase tracking-wider"> Veszélyzóna</h4>
</div>
<p class="text-xs text-red-600 mb-3">A jármű végleges eltávolítása a garázsból. Ez a művelet nem vonható vissza!</p>
<button
@click="openArchiveConfirm"
class="inline-flex items-center gap-2 rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-all hover:bg-red-700 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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Jármű eltávolítása
</button>
</div>
</div>
<!-- -->
@@ -313,7 +426,7 @@
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<!-- Drive Type -->
<!-- Drive Type (DYNAMIC: motorcycle vs car/commercial options) -->
<div>
<label class="mb-1.5 block text-sm font-medium text-slate-700">Hajtáslánc</label>
<select
@@ -321,9 +434,12 @@
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:outline-none focus:ring-2 focus:ring-sf-accent/20 appearance-none cursor-pointer"
>
<option value="">Válassz</option>
<option value="fwd">FWD (Elsőkerék)</option>
<option value="rwd">RWD (Hátsókerék)</option>
<option value="awd">AWD (Összkerék)</option>
<option v-if="form.vehicle_class === 'motorcycle'" value="chain">Lánc</option>
<option v-if="form.vehicle_class === 'motorcycle'" value="shaft">Kardán</option>
<option v-if="form.vehicle_class === 'motorcycle'" value="belt">Szíj</option>
<option v-if="form.vehicle_class !== 'motorcycle'" value="fwd">FWD (Elsőkerék)</option>
<option v-if="form.vehicle_class !== 'motorcycle'" value="rwd">RWD (Hátsókerék)</option>
<option v-if="form.vehicle_class !== 'motorcycle'" value="awd">AWD (Összkerék)</option>
</select>
</div>
@@ -335,10 +451,13 @@
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:outline-none focus:ring-2 focus:ring-sf-accent/20 appearance-none cursor-pointer"
>
<option value="">Válassz</option>
<option value="manual">Manuális</option>
<option value="automatic">Automata</option>
<option value="cvt">CVT</option>
<option value="dct">DCT</option>
<option v-if="form.vehicle_class === 'motorcycle'" value="manual_sequential">Kézi (Szekvenciális)</option>
<option v-if="form.vehicle_class === 'motorcycle'" value="automatic">Automata</option>
<option v-if="form.vehicle_class === 'motorcycle'" value="cvt">CVT</option>
<option v-if="form.vehicle_class !== 'motorcycle'" value="manual">Manuális</option>
<option v-if="form.vehicle_class !== 'motorcycle'" value="automatic">Automata</option>
<option v-if="form.vehicle_class !== 'motorcycle'" value="cvt">CVT</option>
<option v-if="form.vehicle_class !== 'motorcycle'" value="dct">DCT</option>
</select>
</div>
</div>
@@ -363,39 +482,20 @@
<!-- Seat Count -->
<div>
<label class="mb-1.5 block text-sm font-medium text-slate-700">Ülések száma</label>
<select
v-model="form.seat_count"
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:outline-none focus:ring-2 focus:ring-sf-accent/20 appearance-none cursor-pointer"
>
<option value="">Válassz</option>
<option :value="2">2</option>
<option :value="4">4</option>
<option :value="5">5</option>
<option :value="7">7</option>
<option :value="8">8</option>
<option :value="9">9</option>
</select>
</div>
</div>
<!-- Motorcycle-specific: Drivetrain (chain/shaft/belt) -->
<div v-if="form.vehicle_class === 'motorcycle'" class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label class="mb-1.5 block text-sm font-medium text-slate-700">Hajtáslánc (Motor)</label>
<select
v-model="form.mcycle_drivetrain"
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 focus:border-sf-accent focus:outline-none focus:ring-2 focus:ring-sf-accent/20 appearance-none cursor-pointer"
>
<option value="">Válassz</option>
<option value="chain">Lánc</option>
<option value="shaft">Kardán</option>
<option value="belt">Szíj</option>
</select>
<input
v-model.number="form.seat_count"
type="number"
min="1"
max="9"
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-800 placeholder-slate-400 focus:border-sf-accent focus:outline-none focus:ring-2 focus:ring-sf-accent/20"
placeholder="Pl. 3, 5, 7"
/>
</div>
</div>
</div>
<!-- -->
<!-- STEP 3: FELSZERELTSÉG / JSONB -->
<!-- STEP 3: FELSZERELTSÉG / EXTRÁK -->
<!-- -->
<div v-if="step === 3" class="space-y-4">
<!-- Color -->
@@ -439,29 +539,13 @@
<div v-if="form.vehicle_class === 'motorcycle'" class="space-y-3 rounded-xl border border-slate-200 bg-white p-4">
<h4 class="text-sm font-bold text-slate-700">🏍 Motor extrák</h4>
<div class="grid grid-cols-2 gap-3">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.individual_equipment.plexi" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">Plexi</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.individual_equipment.oldaldoboz" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">Oldaldoboz</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.individual_equipment.hatsodoboz" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">Hátsódoboz</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.individual_equipment.kormanyvegtukor" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">Kormányvégtükör</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.individual_equipment.telefontolto" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">Telefontöltő</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.individual_equipment.egyedi_kipufogo" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">Egyedi kipufogó</span>
<label
v-for="extra in motorcycleExtras"
:key="extra.key"
class="flex items-center gap-2 cursor-pointer"
>
<input type="checkbox" v-model="form.individual_equipment[extra.key]" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">{{ extra.label }}</span>
</label>
</div>
</div>
@@ -470,26 +554,21 @@
<div v-if="['heavy_commercial', 'light_commercial'].includes(form.vehicle_class)" class="space-y-3 rounded-xl border border-slate-200 bg-white p-4">
<h4 class="text-sm font-bold text-slate-700">🚛 Haszongépjármű extrák</h4>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Tachográf</label>
<select v-model="form.individual_equipment.tachograf" 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:outline-none focus:ring-2 focus:ring-sf-accent/20 appearance-none cursor-pointer">
<option value="">Válassz</option>
<option value="van">Van</option>
<option value="nincs">Nincs</option>
</select>
<div v-for="extra in commercialExtras" :key="extra.key">
<template v-if="extra.type === 'select'">
<label class="mb-1 block text-sm font-medium text-slate-700">{{ extra.label }}</label>
<select v-model="form.individual_equipment[extra.key]" 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:outline-none focus:ring-2 focus:ring-sf-accent/20 appearance-none cursor-pointer">
<option value="">Válassz</option>
<option v-for="opt in extra.options" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
</select>
</template>
<template v-else-if="extra.type === 'checkbox'">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.individual_equipment[extra.key]" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">{{ extra.label }}</span>
</label>
</template>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Emelőhátfal</label>
<select v-model="form.individual_equipment.emelohatfal" 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:outline-none focus:ring-2 focus:ring-sf-accent/20 appearance-none cursor-pointer">
<option value="">Válassz</option>
<option value="van">Van</option>
<option value="nincs">Nincs</option>
</select>
</div>
<label class="flex items-center gap-2 cursor-pointer col-span-2">
<input type="checkbox" v-model="form.individual_equipment.halofulke" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">Hálófülke</span>
</label>
</div>
</div>
@@ -497,17 +576,13 @@
<div v-if="form.vehicle_class === 'personal'" class="space-y-3 rounded-xl border border-slate-200 bg-white p-4">
<h4 class="text-sm font-bold text-slate-700">🚗 Személyautó extrák</h4>
<div class="grid grid-cols-2 gap-3">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.individual_equipment.vonohorog" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">Vonóhorog</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.individual_equipment.isofix" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">Isofix</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.individual_equipment.napfenyteto" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">Napfénytető</span>
<label
v-for="extra in personalCarExtras"
:key="extra.key"
class="flex items-center gap-2 cursor-pointer"
>
<input type="checkbox" v-model="form.individual_equipment[extra.key]" class="rounded border-slate-300 text-sf-accent focus:ring-sf-accent/20" />
<span class="text-sm text-slate-700">{{ extra.label }}</span>
</label>
</div>
</div>
@@ -544,8 +619,8 @@
</div>
</div>
<!-- DYNAMIC WIZARD FOOTER -->
<div class="flex items-center justify-between border-t border-slate-200 px-6 py-4 bg-slate-50 shrink-0">
<!-- DYNAMIC WIZARD FOOTER (hidden during archive confirmation) -->
<div v-if="!showArchiveConfirm" class="flex items-center justify-between border-t border-slate-200 px-6 py-4 bg-slate-50 shrink-0">
<!-- Left side: navigation buttons -->
<div class="flex items-center gap-2">
<!-- Vissza (visible when step > 0, but hidden in edit mode on step 1 to prevent going back to step 0) -->
@@ -584,15 +659,15 @@
<div class="flex items-center gap-2">
<!-- Mégsem (always visible) -->
<button
@click="$emit('close')"
@click="handleClose"
class="inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-600 transition-all hover:bg-slate-100 cursor-pointer"
>
Mégsem
</button>
<!-- Mentés (visible only on step 3) -->
<!-- Mentés (visible on step 3 OR when editing any step) -->
<button
v-if="step === 3"
v-if="step === 3 || isEditMode"
@click="handleSave"
:disabled="isSaving || !isValid"
class="inline-flex items-center gap-2 rounded-xl bg-emerald-600 px-5 py-2 text-sm font-semibold text-white shadow-sm transition-all hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
@@ -610,15 +685,14 @@
</div>
</div>
</div>
</div>
</Teleport>
</Teleport>
<!-- Toast Notification (sibling root, outside modal Teleport) -->
<Teleport to="body">
<div
v-if="toastVisible"
class="fixed top-6 right-6 z-[99999] flex items-center gap-3 rounded-2xl px-5 py-4 shadow-2xl transition-all duration-300 animate-slide-in"
:class="toastType === 'success' ? 'bg-emerald-600 text-white' : 'bg-red-600 text-white'"
<!-- Toast Notification (sibling root, outside modal Teleport) -->
<Teleport to="body">
<div
v-if="toastVisible"
class="fixed top-6 right-6 z-[99999] flex items-center gap-3 rounded-2xl px-5 py-4 shadow-2xl transition-all duration-300"
:class="toastType === 'success' ? 'bg-emerald-600 text-white' : 'bg-red-600 text-white'"
>
<svg v-if="toastType === 'success'" class="h-5 w-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
@@ -637,11 +711,13 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useVehicleStore } from '../../stores/vehicle'
import { useAuthStore } from '../../stores/auth'
import api from '../../api/axios'
const vehicleStore = useVehicleStore()
const authStore = useAuthStore()
const props = defineProps<{
isOpen: boolean
@@ -693,6 +769,29 @@ const vehicleClasses = [
{ value: 'other', label: 'Egyéb', icon: '🔧' },
]
// ── Category-specific equipment constant arrays (Bug 3 fix) ──
const motorcycleExtras = [
{ key: 'plexi', label: 'Plexi' },
{ key: 'oldaldoboz', label: 'Oldaldoboz' },
{ key: 'hatsodoboz', label: 'Hátsódoboz' },
{ key: 'kormanyvegtukor', label: 'Kormányvégtükör' },
{ key: 'telefontolto', label: 'Telefontöltő' },
{ key: 'egyedi_kipufogo', label: 'Egyedi kipufogó' },
{ key: 'gyorsvalto', label: 'Gyorsváltó' },
]
const commercialExtras = [
{ key: 'tachograf', label: 'Tachográf', type: 'select', options: [{ value: 'van', label: 'Van' }, { value: 'nincs', label: 'Nincs' }] },
{ key: 'emelohatfal', label: 'Emelőhátfal', type: 'select', options: [{ value: 'van', label: 'Van' }, { value: 'nincs', label: 'Nincs' }] },
{ key: 'halofulke', label: 'Hálófülke', type: 'checkbox' },
]
const personalCarExtras = [
{ key: 'vonohorog', label: 'Vonóhorog' },
{ key: 'isofix', label: 'Isofix' },
{ key: 'napfenyteto', label: 'Napfénytető' },
]
// ── Normalization helper ──
function normalizeId(str: string): string {
return str.replace(/[\s-]/g, '').toUpperCase()
@@ -727,9 +826,6 @@ const form = reactive({
door_count: null as number | null,
seat_count: null as number | null,
// Motorcycle-specific (stored in individual_equipment)
mcycle_drivetrain: '',
// STEP 3: Equipment / JSONB
color: '',
ac_type: '',
@@ -740,6 +836,65 @@ const form = reactive({
})
const isSaving = ref(false)
const isClosing = ref(false)
// ── Archive (Soft Delete) state ──
const showArchiveConfirm = ref(false)
const archiveConfirmPlate = ref('')
const archiveFinalMileage = ref<number | null>(null)
const archiveReason = ref('')
const isArchiving = ref(false)
function openArchiveConfirm() {
showArchiveConfirm.value = true
// Üres mezők — a felhasználónak kell kitöltenie
archiveConfirmPlate.value = ''
archiveFinalMileage.value = null
archiveReason.value = ''
}
function cancelArchive() {
showArchiveConfirm.value = false
archiveConfirmPlate.value = ''
archiveFinalMileage.value = null
archiveReason.value = ''
}
const isArchiveValid = computed(() => {
const plate = props.vehicle?.license_plate || ''
return (
archiveConfirmPlate.value.trim().toUpperCase() === plate.trim().toUpperCase() &&
archiveFinalMileage.value !== null &&
archiveFinalMileage.value > 0 &&
archiveReason.value.trim().length > 0
)
})
async function confirmArchive() {
if (!isArchiveValid.value || !props.vehicle?.id) return
isArchiving.value = true
try {
const success = await vehicleStore.archiveVehicle(
props.vehicle.id,
archiveFinalMileage.value,
archiveReason.value
)
if (success) {
showToast('✅ Jármű sikeresen eltávolítva a garázsból', 'success')
showArchiveConfirm.value = false
// Close the modal and refresh the vehicle list
handleClose()
vehicleStore.fetchVehicles()
}
} catch (err: any) {
const errorMsg = err?.response?.data?.detail || err?.message || 'Ismeretlen hiba történt az eltávolítás során.'
showToast(errorMsg, 'error')
} finally {
isArchiving.value = false
}
}
// ── Dirty tracking ──
const isDirty = ref(false)
@@ -752,47 +907,23 @@ watch(
{ deep: true }
)
// ── 🦎 CHAMELEON: Dynamic Trim Levels based on vehicle_class ──
const trimLevelMap: Record<string, { value: string; label: string }[]> = {
personal: [
{ value: 'sedan', label: 'Sedan' },
{ value: 'combi', label: 'Kombi' },
{ value: 'suv', label: 'SUV' },
{ value: 'hatchback', label: 'Ferdehátú' },
{ value: 'cabrio', label: 'Cabrio' },
{ value: 'coupe', label: 'Coupé' },
],
motorcycle: [
{ value: 'enduro', label: 'Enduro' },
{ value: 'sport', label: 'Sport' },
{ value: 'naked', label: 'Naked' },
{ value: 'chopper', label: 'Chopper' },
{ value: 'cruiser', label: 'Cruiser' },
{ value: 'touring', label: 'Touring' },
{ value: 'scooter', label: 'Robogó' },
],
light_commercial: [
{ value: 'box', label: 'Dobozos' },
{ value: 'open_platform', label: 'Nyitott platós' },
{ value: 'tarpaulin', label: 'Ponyvás' },
{ value: 'refrigerated', label: 'Hűtős' },
],
heavy_commercial: [
{ value: 'box', label: 'Dobozos' },
{ value: 'open_platform', label: 'Nyitott platós' },
{ value: 'tarpaulin', label: 'Ponyvás' },
{ value: 'refrigerated', label: 'Hűtős' },
{ value: 'tractor', label: 'Nyergesvontató' },
],
machinery: [
{ value: 'forklift', label: 'Targonca' },
{ value: 'excavator', label: 'Markoló' },
{ value: 'tractor', label: 'Traktor' },
],
}
// ── 🦎 BODY TYPE DICTIONARY: Loaded from API ──
const bodyTypeDictionary = ref<{ id: number; vehicle_class: string; code: string; name_hu: string }[]>([])
// Fetch body types from the API on mount
onMounted(async () => {
try {
const res = await api.get('/catalog/body-types')
bodyTypeDictionary.value = res.data || []
} catch (err) {
console.error('[VehicleFormModal] Failed to load body types:', err)
}
})
const availableTrimLevels = computed(() => {
return trimLevelMap[form.vehicle_class] || []
return bodyTypeDictionary.value
.filter(bt => bt.vehicle_class === form.vehicle_class)
.map(bt => ({ value: bt.code, label: bt.name_hu }))
})
// ── Show door & seat for vehicles with a cabin ──
@@ -809,7 +940,9 @@ const showAcField = computed(() => {
watch(
() => form.vehicle_class,
(newClass) => {
const validValues = trimLevelMap[newClass]?.map(o => o.value) || []
const validValues = bodyTypeDictionary.value
.filter(bt => bt.vehicle_class === newClass)
.map(bt => bt.code)
if (form.trim_level && !validValues.includes(form.trim_level)) {
form.trim_level = ''
}
@@ -820,23 +953,19 @@ watch(
const isStepValid = computed(() => {
switch (step.value) {
case 0:
// Step 0: license_plate and vehicle_class required
return (
form.license_plate.trim().length >= 2 &&
form.vehicle_class.trim().length > 0
)
case 1:
// Step 1: brand, model, fuel_type required
return (
form.brand.trim().length > 0 &&
form.model.trim().length > 0 &&
form.fuel_type.trim().length > 0
)
case 2:
// Step 2: all optional
return true
case 3:
// Step 3: all optional
return true
default:
return false
@@ -857,7 +986,6 @@ const isValid = computed(() => {
// ── Step Navigation ──
async function nextStep() {
if (step.value === 0) {
// Step 0 → Step 1: Live DB check first
const canProceed = await checkVehicleExists()
if (!canProceed) return
}
@@ -869,7 +997,8 @@ function prevStep() {
}
function goToStep(idx: number) {
// Allow going back to any previous step, but only forward if step is valid
// 🚫 BLOCK navigation to step 0 (Identifikáció) in edit mode
if (isEditMode.value && idx === 0) return
if (idx <= step.value) {
step.value = idx
} else if (isStepValid.value) {
@@ -879,6 +1008,10 @@ function goToStep(idx: number) {
// ── Helpers ──
function getStepCircleClass(idx: number) {
// 🚫 Step 0 is locked in edit mode — show muted styling
if (isEditMode.value && idx === 0) {
return 'bg-slate-200 text-slate-400 opacity-50 cursor-not-allowed'
}
if (step.value > idx) return 'bg-emerald-500 text-white'
if (step.value === idx) return 'bg-sf-accent text-white ring-2 ring-sf-accent/30'
return 'bg-slate-200 text-slate-500'
@@ -933,7 +1066,6 @@ function resetForm() {
form.transmission_type = ''
form.door_count = null
form.seat_count = null
form.mcycle_drivetrain = ''
form.color = ''
form.ac_type = ''
form.notes = ''
@@ -954,7 +1086,6 @@ async function checkVehicleExists(): Promise<boolean> {
isChecking.value = true
try {
// Build params: license_plate is required, vin is optional
const params: Record<string, string> = { license_plate: plate }
const vin = normalizeId(form.vin || '')
if (vin) {
@@ -970,7 +1101,6 @@ async function checkVehicleExists(): Promise<boolean> {
return true
} catch (err: any) {
console.error('[VehicleFormModal] checkVehicleExists error:', err)
// If the check fails, allow proceeding (degraded UX)
return true
} finally {
isChecking.value = false
@@ -993,11 +1123,27 @@ function checkDuplicateLicensePlate(plate: string): boolean {
return false
}
// ── Close Handler ──
function handleClose() {
isClosing.value = true
setTimeout(() => {
if (isEditMode.value && props.vehicle) {
resetForm()
} else {
resetForm()
}
emit('close')
// Reset closing state after emit so next open works cleanly
setTimeout(() => {
isClosing.value = false
}, 50)
}, 300)
}
// ── Save Handler ──
async function handleSave() {
if (!isValid.value) return
// Frontend Duplicate Validation (CREATE mode + EDIT mode when plate changed)
const isDuplicate = checkDuplicateLicensePlate(form.license_plate)
if (isDuplicate) {
showToast('Ez a rendszám már szerepel a járműveid között!', 'error')
@@ -1007,7 +1153,6 @@ async function handleSave() {
isSaving.value = true
try {
// Build payload matching AssetCreate schema
const payload: Record<string, any> = {
license_plate: normalizeId(form.license_plate),
brand: form.brand.trim(),
@@ -1016,7 +1161,6 @@ async function handleSave() {
fuel_type: form.fuel_type,
}
// If brand or model changed manually, disconnect the old catalog reference
if (props.vehicle?.id) {
const brandChanged = form.brand.trim() !== (props.vehicle.brand || '')
const modelChanged = form.model.trim() !== (props.vehicle.model || '')
@@ -1042,12 +1186,9 @@ async function handleSave() {
if (form.ac_type) extraEquipment.ac_type = form.ac_type
if (form.notes) extraEquipment.notes = form.notes
if (form.name) extraEquipment.name = form.name
// Save custom_vehicle_class when 'other' is selected
if (form.vehicle_class === 'other' && form.custom_vehicle_class) {
extraEquipment.custom_vehicle_class = form.custom_vehicle_class.trim()
}
// 🦎 CHAMELEON: Category-specific fields → individual_equipment
if (form.mcycle_drivetrain) extraEquipment.mcycle_drivetrain = form.mcycle_drivetrain
// Merge category-specific individual_equipment (motorcycle, commercial, personal extras)
if (form.individual_equipment && Object.keys(form.individual_equipment).length > 0) {
Object.assign(extraEquipment, form.individual_equipment)
@@ -1056,13 +1197,24 @@ async function handleSave() {
payload.individual_equipment = extraEquipment
}
// ── 🛡️ KETTŐS VÉDELEM: Csendes szervezet hozzárendelés ──
// A felhasználó tudta nélkül csatoljuk az organization_id-t a payloadhoz,
// hogy soha ne kerüljön NULL értékkel az adatbázisba.
// Prioritás: active_organization_id > első elérhető szervezet
const silentOrgId =
authStore.user?.active_organization_id ||
authStore.myOrganizations[0]?.organization_id ||
null
if (silentOrgId !== null && !payload.organization_id) {
payload.organization_id = silentOrgId
console.log('[VehicleFormModal] 🛡️ Silent org assignment:', silentOrgId)
}
let savedVehicle: any
if (props.vehicle?.id) {
// EDIT mode: call updateVehicle
savedVehicle = await vehicleStore.updateVehicle(props.vehicle.id, payload)
} else {
// CREATE mode: call createVehicle
savedVehicle = await vehicleStore.createVehicle(payload)
}
@@ -1087,10 +1239,8 @@ watch(
(newVal) => {
if (newVal) {
if (props.vehicle) {
// EDIT mode: skip step 0, go directly to step 1 (Basic Data)
step.value = 1
} else {
// CREATE mode: reset everything, start at step 0
resetForm()
}
}
@@ -1117,7 +1267,6 @@ watch(
form.door_count = newVal.door_count || null
form.trim_level = newVal.trim_level || ''
form.seat_count = newVal.seat_count || null
form.mcycle_drivetrain = newVal.individual_equipment?.mcycle_drivetrain || ''
form.color = newVal.individual_equipment?.color || newVal.color || ''
form.ac_type = newVal.individual_equipment?.ac_type || ''
form.notes = newVal.individual_equipment?.notes || newVal.notes || ''
@@ -1131,7 +1280,6 @@ watch(
delete form.individual_equipment.notes
delete form.individual_equipment.name
delete form.individual_equipment.custom_vehicle_class
delete form.individual_equipment.mcycle_drivetrain
isDirty.value = false
} else {
resetForm()
@@ -1156,4 +1304,28 @@ watch(
.animate-slide-in {
animation: slideIn 0.3s ease-out forwards;
}
@keyframes spinShrinkOut {
from {
transform: scale(1) rotate(0deg);
opacity: 1;
}
to {
transform: scale(0.7) rotate(10deg);
opacity: 0;
}
}
.animate-spin-shrink-out {
animation: spinShrinkOut 0.3s ease-in forwards;
}
/* ── Hide number input spinners ── */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,11 @@
>
<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"
class="relative w-full max-w-3xl mx-4 rounded-2xl border border-slate-200 bg-white shadow-2xl overflow-hidden animate-scale-in"
>
<!-- Header -->
<!--
HEADER License Plate Hero + Vehicle Identity
-->
<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">
@@ -50,71 +52,374 @@
</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"
/>
<!--
LICENSE PLATE HERO
-->
<div class="flex justify-center pt-6 pb-4">
<VehiclePlateBadge
:license-plate="vehicle?.license_plate || ''"
:country-code="vehicle?.countryCode || vehicle?.country_code"
size="lg"
/>
</div>
<!--
3-TAB NAVIGATION
-->
<div class="px-6">
<div class="flex border-b border-slate-200 gap-1">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
class="flex items-center gap-2 px-4 py-3 text-sm font-semibold transition-all border-b-2 -mb-px cursor-pointer"
:class="activeTab === tab.id
? 'border-sf-accent text-sf-accent'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'"
>
<span class="text-base">{{ tab.icon }}</span>
<span>{{ tab.label }}</span>
</button>
</div>
</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>
<!--
TAB CONTENT
-->
<div class="p-6 max-h-[55vh] overflow-y-auto">
<!-- TAB 1: Alapadatok (Physical State) -->
<div v-if="activeTab === 'basics'" class="space-y-4">
<!-- Info Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- 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">{{ engineDisplay || '—' }}</p>
<p v-if="vehicle?.power_kw" class="text-xs text-slate-500 mt-0.5">{{ vehicle.power_kw }} kW ({{ hpDisplay }})</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>
<!-- Body / Karosszéria -->
<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">Karosszéria</p>
<p class="text-sm font-bold text-slate-800">{{ vehicle?.vehicle_class || vehicle?.body_type || '—' }}</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>
<!-- VIN / Alvázszám -->
<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">Alvázszám (VIN)</p>
<p class="text-sm font-bold text-slate-800 font-mono tracking-wider">{{ vehicle?.vin || '—' }}</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>
<!-- Current 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">Jelenlegi 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>
<!-- Transmission -->
<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">Váltó</p>
<p class="text-sm font-bold text-slate-800">{{ vehicle?.transmission_type || '—' }}</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>
<!-- Drive Type -->
<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">Hajtás</p>
<p class="text-sm font-bold text-slate-800">{{ vehicle?.drive_type || '—' }}</p>
</div>
<!-- Fuel Type -->
<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">Üzemanyag</p>
<p class="text-sm font-bold text-slate-800">{{ vehicle?.fuel_type || '—' }}</p>
</div>
<!-- Trim Level / Felszereltség -->
<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">Felszereltség</p>
<p class="text-sm font-bold text-slate-800">{{ vehicle?.trim_level || '—' }}</p>
</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>
<!-- Vehicle Condition Real Trust Score -->
<div class="rounded-xl border border-slate-200 bg-gradient-to-r from-slate-50 to-white p-5 mt-2">
<div class="flex items-center justify-between mb-3">
<p class="text-sm font-bold text-slate-700">Gépjármű Állapot</p>
<span
class="inline-flex items-center rounded-full px-3 py-1 text-xs font-bold"
:class="trustScoreColorClass"
>
{{ trustScoreDisplay }}
</span>
</div>
<div class="h-3 overflow-hidden rounded-full bg-slate-200">
<div
class="h-full rounded-full transition-all duration-1000"
:class="trustScoreBarColorClass"
:style="{ width: trustScorePercent + '%' }"
/>
</div>
<div class="flex justify-between mt-1.5 text-[10px] text-slate-400">
<span>Kopó alkatrészek</span>
<span>Karosszéria</span>
<span>Motor & Erőátvitel</span>
<span>Elektronika</span>
</div>
</div>
</div>
<!-- TAB 2: Pénzügyek / TCO -->
<div v-if="activeTab === 'finance'" class="space-y-5">
<!-- Annual Total Cost Hero -->
<div class="rounded-2xl border border-slate-200 bg-gradient-to-br from-slate-50 to-white p-6 text-center">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Éves Összköltség (TCO)</p>
<p class="text-4xl font-extrabold text-slate-800">1,245,000 Ft</p>
<p class="text-xs text-slate-400 mt-1">~ 3,411 Ft / nap</p>
</div>
<!-- Task 5: Freemium Logic -->
<!-- Free users: simple clean table -->
<template v-if="!isPremium">
<div class="rounded-xl border border-slate-200 bg-slate-50 overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="bg-slate-100 border-b border-slate-200">
<th class="text-left px-4 py-3 font-semibold text-slate-600">Költségtípus</th>
<th class="text-right px-4 py-3 font-semibold text-slate-600">Összeg</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-slate-200">
<td class="px-4 py-2.5 text-slate-600">Biztosítás (KGFB + Casco)</td>
<td class="px-4 py-2.5 text-right font-semibold text-slate-800">320,000 Ft</td>
</tr>
<tr class="border-b border-slate-200">
<td class="px-4 py-2.5 text-slate-600">Gépjárműadó</td>
<td class="px-4 py-2.5 text-right font-semibold text-slate-800">45,000 Ft</td>
</tr>
<tr class="border-b border-slate-200">
<td class="px-4 py-2.5 text-slate-600">Műszaki vizsga</td>
<td class="px-4 py-2.5 text-right font-semibold text-slate-800">18,000 Ft</td>
</tr>
<tr class="border-b border-slate-200">
<td class="px-4 py-2.5 text-slate-600">Tankolás (üzemanyag)</td>
<td class="px-4 py-2.5 text-right font-semibold text-slate-800">520,000 Ft</td>
</tr>
<tr class="border-b border-slate-200">
<td class="px-4 py-2.5 text-slate-600">Szerviz & Karbantartás</td>
<td class="px-4 py-2.5 text-right font-semibold text-slate-800">210,000 Ft</td>
</tr>
<tr>
<td class="px-4 py-2.5 text-slate-600">Parkolás & Útdíj</td>
<td class="px-4 py-2.5 text-right font-semibold text-slate-800">132,000 Ft</td>
</tr>
</tbody>
<tfoot>
<tr class="bg-slate-100 border-t border-slate-200">
<td class="px-4 py-3 font-bold text-slate-700">Összesen</td>
<td class="px-4 py-3 text-right font-bold text-slate-800">1,245,000 Ft</td>
</tr>
</tfoot>
</table>
</div>
</template>
<!-- Premium users: detailed breakdown with charts -->
<template v-if="isPremium">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Fixed Costs -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
<div class="flex items-center gap-2 mb-3">
<span class="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-100 text-blue-600 text-sm">🔒</span>
<p class="text-sm font-bold text-slate-700">Állandó költségek</p>
</div>
<div class="space-y-2.5">
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500">Biztosítás (KGFB + Casco)</span>
<span class="text-sm font-bold text-slate-700">320,000 Ft</span>
</div>
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500">Gépjárműadó</span>
<span class="text-sm font-bold text-slate-700">45,000 Ft</span>
</div>
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500">Műszaki vizsga</span>
<span class="text-sm font-bold text-slate-700">18,000 Ft</span>
</div>
<div class="border-t border-slate-200 pt-2 flex items-center justify-between">
<span class="text-xs font-bold text-slate-600">Állandó összesen</span>
<span class="text-sm font-bold text-slate-800">383,000 Ft</span>
</div>
</div>
</div>
<!-- Running Costs -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
<div class="flex items-center gap-2 mb-3">
<span class="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-100 text-emerald-600 text-sm"></span>
<p class="text-sm font-bold text-slate-700">Folyó költségek</p>
</div>
<div class="space-y-2.5">
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500">Tankolás (üzemanyag)</span>
<span class="text-sm font-bold text-slate-700">520,000 Ft</span>
</div>
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500">Szerviz & Karbantartás</span>
<span class="text-sm font-bold text-slate-700">210,000 Ft</span>
</div>
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500">Parkolás & Útdíj</span>
<span class="text-sm font-bold text-slate-700">132,000 Ft</span>
</div>
<div class="border-t border-slate-200 pt-2 flex items-center justify-between">
<span class="text-xs font-bold text-slate-600">Folyó összesen</span>
<span class="text-sm font-bold text-slate-800">862,000 Ft</span>
</div>
</div>
</div>
</div>
<!-- Premium: Cost distribution chart placeholder -->
<div class="rounded-xl border border-slate-200 bg-gradient-to-br from-slate-50 to-white p-5">
<p class="text-sm font-bold text-slate-700 mb-3">Költségeloszlás (éves)</p>
<div class="space-y-2">
<div>
<div class="flex justify-between text-xs text-slate-500 mb-1">
<span>Biztosítás</span>
<span>26%</span>
</div>
<div class="h-2 rounded-full bg-slate-200 overflow-hidden">
<div class="h-full rounded-full bg-blue-500" style="width: 26%"></div>
</div>
</div>
<div>
<div class="flex justify-between text-xs text-slate-500 mb-1">
<span>Üzemanyag</span>
<span>42%</span>
</div>
<div class="h-2 rounded-full bg-slate-200 overflow-hidden">
<div class="h-full rounded-full bg-emerald-500" style="width: 42%"></div>
</div>
</div>
<div>
<div class="flex justify-between text-xs text-slate-500 mb-1">
<span>Szerviz</span>
<span>17%</span>
</div>
<div class="h-2 rounded-full bg-slate-200 overflow-hidden">
<div class="h-full rounded-full bg-amber-500" style="width: 17%"></div>
</div>
</div>
<div>
<div class="flex justify-between text-xs text-slate-500 mb-1">
<span>Egyéb</span>
<span>15%</span>
</div>
<div class="h-2 rounded-full bg-slate-200 overflow-hidden">
<div class="h-full rounded-full bg-purple-500" style="width: 15%"></div>
</div>
</div>
</div>
</div>
</template>
<!-- TCO per km hint (visible to all) -->
<div class="rounded-xl border border-slate-200 bg-slate-50 p-3 text-center">
<p class="text-xs text-slate-500">
<span class="font-semibold text-slate-700">TCO / km:</span> ~ 62 Ft/km
<span class="text-slate-300 mx-2">|</span>
<span class="font-semibold text-slate-700">Havi átlag:</span> ~ 103,750 Ft
</p>
</div>
</div>
<!-- TAB 3: Figyelmeztetések & Időpontok -->
<div v-if="activeTab === 'alerts'" class="space-y-4">
<!-- Task 6: Statistics moved here from Tab 2 -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4 text-center">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Átlagos Fogyasztás</p>
<p class="text-2xl font-extrabold text-slate-800">
{{ vehicle?.fuel_type === 'electric' ? '—' : '8.2' }} <span class="text-sm font-semibold text-slate-500">{{ vehicle?.fuel_type === 'electric' ? 'kWh/100km' : 'l/100km' }}</span>
</p>
</div>
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4 text-center">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Átlagos Km Költség</p>
<p class="text-2xl font-extrabold text-slate-800">62 <span class="text-sm font-semibold text-slate-500">Ft/km</span></p>
</div>
</div>
<p class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">Idővonal Közelgő események</p>
<div class="space-y-3">
<!-- Alert 1: Insurance renewal -->
<div class="flex items-start gap-4 rounded-xl border border-amber-200 bg-amber-50/50 p-4">
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-amber-100 text-amber-600 text-base shrink-0">📋</span>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="text-sm font-bold text-slate-800">Kötelező biztosítás fordulónap</p>
<span class="inline-flex items-center rounded-full bg-amber-100 px-2.5 py-0.5 text-[10px] font-bold text-amber-700 uppercase">30 nap</span>
</div>
<p class="text-xs text-slate-500 mt-0.5">Lemondási határidő: 2026. 07. 12. Jelenlegi díj: 85,000 Ft/év</p>
<div class="flex gap-2 mt-2">
<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-[10px] font-semibold text-red-600"> Lemondási határidő</span>
</div>
</div>
</div>
<!-- Alert 2: MOT / Műszaki vizsga -->
<div class="flex items-start gap-4 rounded-xl border border-red-200 bg-red-50/50 p-4">
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-red-100 text-red-600 text-base shrink-0">🔧</span>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="text-sm font-bold text-slate-800">Műszaki vizsga lejárata</p>
<span class="inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-[10px] font-bold text-red-700 uppercase">14 nap</span>
</div>
<p class="text-xs text-slate-500 mt-0.5">Lejárat: 2026. 06. 26. Hátralévő napok: 14</p>
<div class="flex gap-2 mt-2">
<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-[10px] font-semibold text-red-600">🔴 Sürgős</span>
</div>
</div>
</div>
<!-- Alert 3: Next service -->
<div class="flex items-start gap-4 rounded-xl border border-blue-200 bg-blue-50/50 p-4">
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-blue-100 text-blue-600 text-base shrink-0"></span>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="text-sm font-bold text-slate-800">Következő szerviz</p>
<span class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-[10px] font-bold text-blue-700 uppercase">45 nap</span>
</div>
<p class="text-xs text-slate-500 mt-0.5">
{{ vehicle?.current_mileage
? `Jelenleg: ${vehicle.current_mileage.toLocaleString()} km — Következő: ${(vehicle.current_mileage + 15000).toLocaleString()} km`
: '15,000 km vagy 1 év' }}
</p>
<div class="flex gap-2 mt-2">
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-[10px] font-semibold text-blue-600">📅 Tervezett</span>
</div>
</div>
</div>
<!-- Alert 4: Seasonal tire change -->
<div class="flex items-start gap-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-200 text-slate-500 text-base shrink-0">🛞</span>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="text-sm font-bold text-slate-800">Szezonális gumicsere</p>
<span class="inline-flex items-center rounded-full bg-slate-200 px-2.5 py-0.5 text-[10px] font-bold text-slate-600 uppercase">90 nap</span>
</div>
<p class="text-xs text-slate-500 mt-0.5">Téli Nyári gumi: 2026. 09. 10. (ajánlott időszak)</p>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<!--
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')"
@@ -122,7 +427,9 @@
>
Bezárás
</button>
<!-- Task 1: Szerkesztés gomb csak az Alapadatok fülön látható -->
<button
v-if="activeTab === 'basics'"
@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"
>
@@ -138,8 +445,9 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ref, computed, watch } from 'vue'
import type { VehicleData } from '../../types/vehicle'
import { useAuthStore } from '../../stores/auth'
import VehiclePlateBadge from './VehiclePlateBadge.vue'
const props = defineProps<{
@@ -153,16 +461,87 @@ const emit = defineEmits<{
'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'
const authStore = useAuthStore()
// ── Tab state ──
const activeTab = ref<'basics' | 'finance' | 'alerts'>('basics')
const tabs = [
{ id: 'basics' as const, label: 'Alapadatok', icon: '📋' },
{ id: 'finance' as const, label: 'Pénzügyek (TCO)', icon: '💰' },
{ id: 'alerts' as const, label: 'Figyelmeztetések', icon: '🔔' },
]
// ── Task 2: Reset activeTab to 'basics' every time modal opens ──
watch(() => props.isOpen, (newVal) => {
if (newVal) {
activeTab.value = 'basics'
}
})
// ── Task 4: Trust Score computed helpers ──
const trustScorePercent = computed(() => {
const v = props.vehicle
if (!v) return 0
// Use condition_score from backend (0-100), fallback to a derived value
if (v.condition_score !== undefined && v.condition_score !== null) {
return Math.round(Math.min(100, Math.max(0, v.condition_score)))
}
// Fallback: derive from is_verified and status
if (v.is_verified && v.status === 'active') return 85
if (v.status === 'active') return 65
return 40
})
const trustScoreLabel = computed(() => {
const pct = trustScorePercent.value
if (pct >= 90) return 'Kiváló'
if (pct >= 75) return 'Jó'
if (pct >= 55) return 'Átlagos'
if (pct >= 35) return 'Gyenge'
return 'Kritikus'
})
const trustScoreDisplay = computed(() => {
return `${trustScorePercent.value}% (${trustScoreLabel.value})`
})
const trustScoreColorClass = computed(() => {
const pct = trustScorePercent.value
if (pct >= 75) return 'bg-emerald-100 text-emerald-700'
if (pct >= 55) return 'bg-amber-100 text-amber-700'
if (pct >= 35) return 'bg-orange-100 text-orange-700'
return 'bg-red-100 text-red-700'
})
const trustScoreBarColorClass = computed(() => {
const pct = trustScorePercent.value
if (pct >= 75) return 'bg-gradient-to-r from-emerald-400 to-emerald-600'
if (pct >= 55) return 'bg-gradient-to-r from-amber-400 to-amber-600'
if (pct >= 35) return 'bg-gradient-to-r from-orange-400 to-orange-600'
return 'bg-gradient-to-r from-red-400 to-red-600'
})
// ── Task 5: Freemium check ──
const isPremium = computed(() => {
return authStore.user?.subscription_plan === 'premium' || authStore.user?.subscription_plan === 'enterprise'
})
// ── Computed helpers ──
const engineDisplay = computed(() => {
const v = props.vehicle
if (!v) return '—'
if (v.engine) return v.engine
const parts: string[] = []
if (v.engine_capacity) parts.push(`${(v.engine_capacity / 1000).toFixed(1)}L`)
if (v.fuel_type) parts.push(v.fuel_type)
return parts.length > 0 ? parts.join(' ') : '—'
})
const hpDisplay = computed(() => {
const kw = props.vehicle?.power_kw
if (!kw) return '—'
return `${Math.round(kw * 1.341)} LE`
})
</script>