egyedi jármű szerkesztés előtti mentés
This commit is contained in:
311
frontend/src/components/dashboard/ComplexExpenseModal.vue
Normal file
311
frontend/src/components/dashboard/ComplexExpenseModal.vue
Normal 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>
|
||||
@@ -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)
|
||||
|
||||
200
frontend/src/components/dashboard/SimpleFuelModal.vue
Normal file
200
frontend/src/components/dashboard/SimpleFuelModal.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
1159
frontend/src/components/dashboard/VehicleFormModal_original.vue
Normal file
1159
frontend/src/components/dashboard/VehicleFormModal_original.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user