import { defineStore } from 'pinia' import { ref, computed } from 'vue' import api from '../api/axios' import type { AssetFinancials, VehicleInsurancePolicy, VehicleTaxObligation } from '../types/vehicle' /** * Vehicle data shape returned by GET /assets/vehicles * Based on the AssetResponse Pydantic schema from the backend. */ export interface Vehicle { id: string vin: string | null license_plate: string | null name: string | null catalog_id: number | null // Classification vehicle_class: string | null brand: string | null model: string | null trim_level: string | null // Technical specs fuel_type: string | null engine_capacity: number | null power_kw: number | null torque_nm: number | null cylinder_layout: string | null transmission_type: string | null drive_type: string | null euro_classification: string | null curb_weight: number | null max_weight: number | null door_count: number | null seat_count: number | null // Status current_mileage: number condition_score: number status: string is_verified: boolean // Timeline year_of_manufacture: number | null created_at: string updated_at: string | null // Warranty is_under_warranty: boolean warranty_expiry_date: string | null // Registration Documents registration_certificate_number?: string | null registration_certificate_validity?: string | null vehicle_registration_document_number?: string | null // ── New Admin Fields ──────────────────────────────────────────────── registration_country?: string | null first_domestic_registration_date?: string | null import_country?: string | null title_document_number?: string | null engine_number?: string | null number_of_previous_owners?: number | null // Sales is_for_sale: boolean price: number | null currency: string // Primary vehicle flag (from backend individual_equipment JSONB) is_primary: boolean // Organization association (from backend AssetResponse) current_organization_id: number | null owner_organization_id: number | null // ── Finance / Leasing ─────────────────────────────────────────────── asset_financials?: AssetFinancials | null insurance_policies?: VehicleInsurancePolicy[] | null tax_obligations?: VehicleTaxObligation[] | null // JSONB individual_equipment from backend (car_specs, ev_specs, dates, etc.) individual_equipment?: { car_specs?: { body_type?: string ac_type?: string cylinder_layout?: string features?: string[] } ev_specs?: { battery_capacity_kwh?: number battery_soh?: number ac_connector?: string ac_power_kw?: number dc_connector?: string dc_power_kw?: number range_wltp?: number range_highway?: number range_winter?: number fast_charge_support?: boolean green_plate_eligible?: boolean } dates?: { mot_expiry?: string document_expiry?: string } [key: string]: any } } export const useVehicleStore = defineStore('vehicle', () => { // ── State ────────────────────────────────────────────────────────── const vehicles = ref([]) const isLoading = ref(false) const error = ref(null) // ── Getters ──────────────────────────────────────────────────────── /** * Sorted vehicles: primary/favorite vehicles first, then alphabetical by brand/model. * Used across all vehicle list displays (dashboard card, manager, etc.). */ const sortedVehicles = computed(() => { return [...vehicles.value].sort((a, b) => { // Primary vehicles first if (a.is_primary && !b.is_primary) return -1 if (!a.is_primary && b.is_primary) return 1 // Then alphabetical by brand + model const nameA = `${a.brand || ''} ${a.model || ''} ${a.license_plate || ''}`.trim().toLowerCase() const nameB = `${b.brand || ''} ${b.model || ''} ${b.license_plate || ''}`.trim().toLowerCase() return nameA.localeCompare(nameB) }) }) // ── Actions ──────────────────────────────────────────────────────── /** * Fetch all vehicles/assets for the current user or their organization. * GET /assets/vehicles */ // P0 BUGFIX (2026-07-28): Deduplication guard to prevent multiple simultaneous // calls from DashboardView.onMounted() and child components. let _vehicleFetchInProgress = false /** * P0 BUGFIX (2026-07-28): Context-switching data leak fix. * Clears the local vehicle cache and resets the deduplication guard. * Called by DashboardView before re-fetching after organization switch. */ function clearCache() { _vehicleFetchInProgress = false vehicles.value = [] error.value = null } async function fetchVehicles() { if (_vehicleFetchInProgress) { // Skip duplicate: a fetch is already in flight return vehicles.value } _vehicleFetchInProgress = true isLoading.value = true error.value = null try { const res = await api.get('/assets/vehicles') vehicles.value = res.data as Vehicle[] } catch (err: any) { const message = err.response?.data?.detail || err.response?.data?.message || 'Nem sikerült betölteni a járműveket. Ellenőrizd a kapcsolatot.' error.value = message console.error('[VehicleStore] fetchVehicles error:', err) } finally { isLoading.value = false _vehicleFetchInProgress = false } } /** * Fetch a single vehicle by its ID. * GET /assets/vehicles/{id} * Used to refresh vehicle data after odometer updates or other mutations. */ async function fetchVehicleById(id: string): Promise { try { const res = await api.get(`/assets/vehicles/${id}`) const updated = res.data as Vehicle // Update the vehicle in local state const index = vehicles.value.findIndex(v => v.id === id) if (index !== -1) { vehicles.value[index] = updated } return updated } catch (err: any) { console.error('[VehicleStore] fetchVehicleById error:', err) return null } } /** * Create a new vehicle via POST /assets/vehicles. * On success, adds the new vehicle to the local state and refreshes the list. */ async function createVehicle(vehicleData: Record): Promise { isLoading.value = true error.value = null try { const res = await api.post('/assets/vehicles', vehicleData) const newVehicle = res.data as Vehicle // Add the new vehicle to local state vehicles.value.unshift(newVehicle) return newVehicle } catch (err: any) { // Log full error response for debugging (422 Validation Error details, etc.) if (err.response) { console.error('[VehicleStore] createVehicle error response:', { status: err.response.status, data: err.response.data, headers: err.response.headers, }) } else { console.error('[VehicleStore] createVehicle network error:', err.message) } const detail = err.response?.data?.detail const message = Array.isArray(detail) ? detail.map((d: any) => `${d.loc?.join('.')}: ${d.msg}`).join('; ') : detail || err.response?.data?.message || 'Nem sikerült létrehozni a járművet.' error.value = message throw err } finally { isLoading.value = false } } /** * Update an existing vehicle via PUT /assets/vehicles/{id}. * On success, updates the vehicle in the local state. */ async function updateVehicle(id: string, vehicleData: Record): Promise { isLoading.value = true error.value = null try { const res = await api.put(`/assets/vehicles/${id}`, vehicleData) const updatedVehicle = res.data as Vehicle // Update the vehicle in local state const index = vehicles.value.findIndex(v => v.id === id) if (index !== -1) { vehicles.value[index] = updatedVehicle } return updatedVehicle } catch (err: any) { // Log full error response for debugging (422 Validation Error details, etc.) if (err.response) { console.error('[VehicleStore] updateVehicle error response:', { status: err.response.status, data: err.response.data, headers: err.response.headers, }) } else { console.error('[VehicleStore] updateVehicle network error:', err.message) } const detail = err.response?.data?.detail const message = Array.isArray(detail) ? detail.map((d: any) => `${d.loc?.join('.')}: ${d.msg}`).join('; ') : detail || err.response?.data?.message || 'Nem sikerült frissíteni a járművet.' error.value = message throw err } finally { isLoading.value = false } } /** * Optimistically set a vehicle as the primary vehicle. * Unsets any other primary vehicle in the local state. */ function setPrimaryVehicle(vehicleId: string | number) { vehicles.value = vehicles.value.map(v => ({ ...v, is_primary: v.id === vehicleId, })) console.log('[VehicleStore] setPrimaryVehicle:', vehicleId) } /** * Archive (soft delete) a vehicle via POST /assets/vehicles/{id}/archive. * On success, removes the vehicle from the local state. */ async function archiveVehicle(id: string, finalMileage: number, archiveReason: string): Promise { isLoading.value = true error.value = null try { await api.post(`/assets/vehicles/${id}/archive`, { final_mileage: finalMileage, archive_reason: archiveReason, }) // Remove the vehicle from local state vehicles.value = vehicles.value.filter(v => v.id !== id) return true } catch (err: any) { if (err.response) { console.error('[VehicleStore] archiveVehicle error response:', { status: err.response.status, data: err.response.data, }) } else { console.error('[VehicleStore] archiveVehicle network error:', err.message) } const detail = err.response?.data?.detail const message = Array.isArray(detail) ? detail.map((d: any) => `${d.loc?.join('.')}: ${d.msg}`).join('; ') : detail || err.response?.data?.message || 'Nem sikerült eltávolítani a járművet.' error.value = message throw err } finally { isLoading.value = false } } // ── Finance Query Methods ────────────────────────────────────────── /** * Fetch asset financials for a vehicle. * GET /assets/vehicles/{id}/financials */ async function fetchAssetFinancials(assetId: string): Promise { try { const res = await api.get(`/assets/vehicles/${assetId}/financials`) return res.data as AssetFinancials } catch (err: any) { console.error('[VehicleStore] fetchAssetFinancials error:', err) return null } } /** * Save asset financials for a vehicle (create or update). * PUT /assets/vehicles/{id}/financials */ async function saveAssetFinancials(assetId: string, data: Partial): Promise { try { const res = await api.put(`/assets/vehicles/${assetId}/financials`, data) return res.data as AssetFinancials } catch (err: any) { console.error('[VehicleStore] saveAssetFinancials error:', err) throw err } } /** * Fetch insurance policies for a vehicle. * GET /assets/vehicles/{id}/insurance */ async function fetchInsurancePolicies(assetId: string): Promise { try { const res = await api.get(`/assets/vehicles/${assetId}/insurance`) return res.data as VehicleInsurancePolicy[] } catch (err: any) { console.error('[VehicleStore] fetchInsurancePolicies error:', err) return [] } } /** * Create a new insurance policy for a vehicle. * POST /assets/vehicles/{id}/insurance */ async function createInsurancePolicy(assetId: string, data: Partial): Promise { try { const res = await api.post(`/assets/vehicles/${assetId}/insurance`, data) return res.data as VehicleInsurancePolicy } catch (err: any) { console.error('[VehicleStore] createInsurancePolicy error:', err) throw err } } /** * Delete an insurance policy. * DELETE /assets/vehicles/{assetId}/insurance/{policyId} */ async function deleteInsurancePolicy(assetId: string, policyId: number): Promise { try { await api.delete(`/assets/vehicles/${assetId}/insurance/${policyId}`) return true } catch (err: any) { console.error('[VehicleStore] deleteInsurancePolicy error:', err) return false } } /** * Fetch tax obligations for a vehicle. * GET /assets/vehicles/{id}/taxes */ async function fetchTaxObligations(assetId: string): Promise { try { const res = await api.get(`/assets/vehicles/${assetId}/taxes`) return res.data as VehicleTaxObligation[] } catch (err: any) { console.error('[VehicleStore] fetchTaxObligations error:', err) return [] } } /** * Create a new tax obligation for a vehicle. * POST /assets/vehicles/{id}/taxes */ async function createTaxObligation(assetId: string, data: Partial): Promise { try { const res = await api.post(`/assets/vehicles/${assetId}/taxes`, data) return res.data as VehicleTaxObligation } catch (err: any) { console.error('[VehicleStore] createTaxObligation error:', err) throw err } } /** * Delete a tax obligation. * DELETE /assets/vehicles/{assetId}/taxes/{taxId} */ async function deleteTaxObligation(assetId: string, taxId: number): Promise { try { await api.delete(`/assets/vehicles/${assetId}/taxes/${taxId}`) return true } catch (err: any) { console.error('[VehicleStore] deleteTaxObligation error:', err) return false } } return { // State vehicles, isLoading, error, // Getters sortedVehicles, // Actions clearCache, fetchVehicles, fetchVehicleById, createVehicle, updateVehicle, setPrimaryVehicle, archiveVehicle, // Finance Actions fetchAssetFinancials, saveAssetFinancials, fetchInsurancePolicies, createInsurancePolicy, deleteInsurancePolicy, fetchTaxObligations, createTaxObligation, deleteTaxObligation, } })