diff --git a/frontend/src/features/ownership-costs/api/ownership-costs.api.ts b/frontend/src/features/ownership-costs/api/ownership-costs.api.ts index 57c300b..248d754 100644 --- a/frontend/src/features/ownership-costs/api/ownership-costs.api.ts +++ b/frontend/src/features/ownership-costs/api/ownership-costs.api.ts @@ -1,60 +1,28 @@ /** - * @ai-summary API calls for ownership-costs feature + * @ai-summary API client for ownership-costs feature */ import { apiClient } from '../../../core/api/client'; -import { - OwnershipCost, - CreateOwnershipCostRequest, - UpdateOwnershipCostRequest, - OwnershipCostStats -} from '../types/ownership-costs.types'; +import type { CreateOwnershipCostRequest, OwnershipCost, UpdateOwnershipCostRequest } from '../types/ownership-costs.types'; export const ownershipCostsApi = { - /** - * Get all ownership costs for a vehicle - */ - getByVehicle: async (vehicleId: string): Promise => { - const response = await apiClient.get(`/ownership-costs/vehicle/${vehicleId}`); - return response.data; + async list(filters?: { vehicleId?: string }) { + const res = await apiClient.get('/ownership-costs', { params: filters }); + return res.data; }, - - /** - * Get a single ownership cost by ID - */ - getById: async (id: string): Promise => { - const response = await apiClient.get(`/ownership-costs/${id}`); - return response.data; + async get(id: string) { + const res = await apiClient.get(`/ownership-costs/${id}`); + return res.data; }, - - /** - * Create a new ownership cost - */ - create: async (data: CreateOwnershipCostRequest): Promise => { - const response = await apiClient.post('/ownership-costs', data); - return response.data; + async create(payload: CreateOwnershipCostRequest) { + const res = await apiClient.post('/ownership-costs', payload); + return res.data; }, - - /** - * Update an existing ownership cost - */ - update: async (id: string, data: UpdateOwnershipCostRequest): Promise => { - const response = await apiClient.put(`/ownership-costs/${id}`, data); - return response.data; + async update(id: string, payload: UpdateOwnershipCostRequest) { + const res = await apiClient.put(`/ownership-costs/${id}`, payload); + return res.data; }, - - /** - * Delete an ownership cost - */ - delete: async (id: string): Promise => { + async remove(id: string) { await apiClient.delete(`/ownership-costs/${id}`); }, - - /** - * Get aggregated cost stats for a vehicle - */ - getVehicleStats: async (vehicleId: string): Promise => { - const response = await apiClient.get(`/ownership-costs/vehicle/${vehicleId}/stats`); - return response.data; - }, }; diff --git a/frontend/src/features/ownership-costs/components/OwnershipCostForm.tsx b/frontend/src/features/ownership-costs/components/OwnershipCostForm.tsx index 8030b9f..e7c59c0 100644 --- a/frontend/src/features/ownership-costs/components/OwnershipCostForm.tsx +++ b/frontend/src/features/ownership-costs/components/OwnershipCostForm.tsx @@ -1,214 +1,235 @@ /** - * @ai-summary Form component for adding/editing ownership costs + * @ai-summary Form component for creating/editing ownership costs */ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { Button } from '../../../shared-minimal/components/Button'; -import { - OwnershipCost, - OwnershipCostType, - CostInterval, - COST_TYPE_LABELS, - INTERVAL_LABELS -} from '../types/ownership-costs.types'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import dayjs from 'dayjs'; +import { useCreateOwnershipCost } from '../hooks/useOwnershipCosts'; +import type { OwnershipCostType } from '../types/ownership-costs.types'; +import { useVehicles } from '../../vehicles/hooks/useVehicles'; +import type { Vehicle } from '../../vehicles/types/vehicles.types'; +import { useDocumentsList } from '../../documents/hooks/useDocuments'; +import type { DocumentRecord } from '../../documents/types/documents.types'; interface OwnershipCostFormProps { - vehicleId: string; - initialData?: OwnershipCost; - onSubmit: (data: { - costType: OwnershipCostType; - description?: string; - amount: number; - interval: CostInterval; - startDate: string; - endDate?: string; - }) => Promise; - onCancel: () => void; - loading?: boolean; + onSuccess?: () => void; + onCancel?: () => void; } -export const OwnershipCostForm: React.FC = ({ - initialData, - onSubmit, - onCancel, - loading, -}) => { - const [costType, setCostType] = useState(initialData?.costType || 'insurance'); - const [description, setDescription] = useState(initialData?.description || ''); - const [amount, setAmount] = useState(initialData?.amount?.toString() || ''); - const [interval, setInterval] = useState(initialData?.interval || 'monthly'); - const [startDate, setStartDate] = useState(initialData?.startDate || new Date().toISOString().split('T')[0]); - const [endDate, setEndDate] = useState(initialData?.endDate || ''); - const [error, setError] = useState(null); +export const OwnershipCostForm: React.FC = ({ onSuccess, onCancel }) => { + const [vehicleID, setVehicleID] = React.useState(''); + const [costType, setCostType] = React.useState('insurance'); + const [amount, setAmount] = React.useState(''); + const [description, setDescription] = React.useState(''); + const [periodStart, setPeriodStart] = React.useState(''); + const [periodEnd, setPeriodEnd] = React.useState(''); + const [notes, setNotes] = React.useState(''); + const [documentID, setDocumentID] = React.useState(''); + const [error, setError] = React.useState(null); - useEffect(() => { - if (initialData) { - setCostType(initialData.costType); - setDescription(initialData.description || ''); - setAmount(initialData.amount.toString()); - setInterval(initialData.interval); - setStartDate(initialData.startDate); - setEndDate(initialData.endDate || ''); - } - }, [initialData]); + const { data: vehicles } = useVehicles(); + const { data: documents } = useDocumentsList({ vehicleId: vehicleID }); + const create = useCreateOwnershipCost(); + + const resetForm = () => { + setVehicleID(''); + setCostType('insurance'); + setAmount(''); + setDescription(''); + setPeriodStart(''); + setPeriodEnd(''); + setNotes(''); + setDocumentID(''); + setError(null); + }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); - // Validate amount + if (!vehicleID) { + setError('Please select a vehicle.'); + return; + } + const parsedAmount = parseFloat(amount); - if (isNaN(parsedAmount) || parsedAmount < 0) { - setError('Please enter a valid amount'); - return; - } - - // Validate dates - if (!startDate) { - setError('Start date is required'); - return; - } - - if (endDate && new Date(endDate) < new Date(startDate)) { - setError('End date must be after start date'); + if (isNaN(parsedAmount) || parsedAmount <= 0) { + setError('Please enter a valid positive amount.'); return; } try { - await onSubmit({ - costType, - description: description.trim() || undefined, + await create.mutateAsync({ + vehicleId: vehicleID, + documentId: documentID || undefined, + costType: costType, amount: parsedAmount, - interval, - startDate, - endDate: endDate || undefined, + description: description.trim() || undefined, + periodStart: periodStart || undefined, + periodEnd: periodEnd || undefined, + notes: notes.trim() || undefined, }); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to save cost'; - setError(message); + + resetForm(); + onSuccess?.(); + } catch (err: any) { + setError(err?.message || 'Failed to create ownership cost'); } }; - const isEditMode = !!initialData; + const vehicleLabel = (v: Vehicle) => { + if (v.nickname && v.nickname.trim().length > 0) return v.nickname.trim(); + const parts = [v.year, v.make, v.model, v.trimLevel].filter(Boolean); + const primary = parts.join(' ').trim(); + if (primary.length > 0) return primary; + if (v.vin && v.vin.length > 0) return v.vin; + return v.id.slice(0, 8) + '...'; + }; + + const documentLabel = (doc: DocumentRecord) => { + return doc.title || `Document ${doc.id.slice(0, 8)}`; + }; return ( -
- {error && ( -
- {error} -
- )} + + +
+
+ + +
-
- - -
+
+ + +
-
- - setDescription(e.target.value)} - placeholder="e.g., Geico Full Coverage" - className="w-full px-3 py-2 border rounded-md min-h-[44px] bg-white text-gray-900 border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:placeholder-canna" - style={{ fontSize: '16px' }} - /> -
+
+ +
+ $ + setAmount(e.target.value)} + required + /> +
+
-
-
- - setAmount(e.target.value)} - placeholder="0.00" - inputMode="decimal" - step="0.01" - min="0" - required - className="w-full px-3 py-2 border rounded-md min-h-[44px] bg-white text-gray-900 border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:placeholder-canna" - style={{ fontSize: '16px' }} - /> +
+ + setDescription(e.target.value)} + /> +
+ +
+ setPeriodStart(newValue?.format('YYYY-MM-DD') || '')} + format="MM/DD/YYYY" + slotProps={{ + textField: { + fullWidth: true, + sx: { + '& .MuiOutlinedInput-root': { + minHeight: 44, + }, + }, + }, + }} + /> +
+
+ setPeriodEnd(newValue?.format('YYYY-MM-DD') || '')} + format="MM/DD/YYYY" + slotProps={{ + textField: { + fullWidth: true, + sx: { + '& .MuiOutlinedInput-root': { + minHeight: 44, + }, + }, + }, + }} + /> +
+ +
+ +