476 lines
15 KiB
TypeScript
476 lines
15 KiB
TypeScript
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<Vehicle[]>([])
|
|
const isLoading = ref(false)
|
|
const error = ref<string | null>(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<Vehicle[]>(() => {
|
|
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<Vehicle | null> {
|
|
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<string, any>): Promise<Vehicle | null> {
|
|
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<string, any>): Promise<Vehicle | null> {
|
|
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<boolean> {
|
|
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<AssetFinancials | null> {
|
|
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<AssetFinancials>): Promise<AssetFinancials | null> {
|
|
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<VehicleInsurancePolicy[]> {
|
|
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<VehicleInsurancePolicy>): Promise<VehicleInsurancePolicy | null> {
|
|
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<boolean> {
|
|
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<VehicleTaxObligation[]> {
|
|
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<VehicleTaxObligation>): Promise<VehicleTaxObligation | null> {
|
|
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<boolean> {
|
|
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,
|
|
}
|
|
})
|