diff --git a/frontend/src/features/documents/components/DocumentForm.tsx b/frontend/src/features/documents/components/DocumentForm.tsx index 129e3ed..41bfdb8 100644 --- a/frontend/src/features/documents/components/DocumentForm.tsx +++ b/frontend/src/features/documents/components/DocumentForm.tsx @@ -6,41 +6,81 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import dayjs from 'dayjs'; -import { useCreateDocument } from '../hooks/useDocuments'; +import { useCreateDocument, useUpdateDocument, useAddSharedVehicle, useRemoveVehicleFromDocument } from '../hooks/useDocuments'; import { documentsApi } from '../api/documents.api'; -import type { DocumentType } from '../types/documents.types'; +import type { DocumentType, DocumentRecord } from '../types/documents.types'; import { useVehicles } from '../../vehicles/hooks/useVehicles'; import type { Vehicle } from '../../vehicles/types/vehicles.types'; import { useTierAccess } from '../../../core/hooks/useTierAccess'; interface DocumentFormProps { + mode?: 'create' | 'edit'; + initialValues?: Partial; + documentId?: string; onSuccess?: () => void; onCancel?: () => void; } -export const DocumentForm: React.FC = ({ onSuccess, onCancel }) => { - const [documentType, setDocumentType] = React.useState('insurance'); - const [vehicleID, setVehicleID] = React.useState(''); - const [title, setTitle] = React.useState(''); - const [notes, setNotes] = React.useState(''); +export const DocumentForm: React.FC = ({ + mode = 'create', + initialValues, + documentId, + onSuccess, + onCancel +}) => { + const [documentType, setDocumentType] = React.useState( + initialValues?.documentType || 'insurance' + ); + const [vehicleID, setVehicleID] = React.useState(initialValues?.vehicleId || ''); + const [title, setTitle] = React.useState(initialValues?.title || ''); + const [notes, setNotes] = React.useState(initialValues?.notes || ''); // Insurance fields - const [insuranceCompany, setInsuranceCompany] = React.useState(''); - const [policyNumber, setPolicyNumber] = React.useState(''); - const [effectiveDate, setEffectiveDate] = React.useState(''); - const [expirationDate, setExpirationDate] = React.useState(''); - const [bodilyInjuryPerson, setBodilyInjuryPerson] = React.useState(''); - const [bodilyInjuryIncident, setBodilyInjuryIncident] = React.useState(''); - const [propertyDamage, setPropertyDamage] = React.useState(''); - const [premium, setPremium] = React.useState(''); + const [insuranceCompany, setInsuranceCompany] = React.useState( + initialValues?.details?.insuranceCompany || '' + ); + const [policyNumber, setPolicyNumber] = React.useState( + initialValues?.details?.policyNumber || '' + ); + const [effectiveDate, setEffectiveDate] = React.useState( + initialValues?.issuedDate || '' + ); + const [expirationDate, setExpirationDate] = React.useState( + initialValues?.expirationDate || '' + ); + const [bodilyInjuryPerson, setBodilyInjuryPerson] = React.useState( + initialValues?.details?.bodilyInjuryPerson || '' + ); + const [bodilyInjuryIncident, setBodilyInjuryIncident] = React.useState( + initialValues?.details?.bodilyInjuryIncident || '' + ); + const [propertyDamage, setPropertyDamage] = React.useState( + initialValues?.details?.propertyDamage || '' + ); + const [premium, setPremium] = React.useState( + initialValues?.details?.premium ? String(initialValues.details.premium) : '' + ); // Registration fields - const [licensePlate, setLicensePlate] = React.useState(''); - const [registrationExpirationDate, setRegistrationExpirationDate] = React.useState(''); - const [registrationCost, setRegistrationCost] = React.useState(''); + const [licensePlate, setLicensePlate] = React.useState( + initialValues?.details?.licensePlate || '' + ); + const [registrationExpirationDate, setRegistrationExpirationDate] = React.useState( + initialValues?.expirationDate || '' + ); + const [registrationCost, setRegistrationCost] = React.useState( + initialValues?.details?.cost ? String(initialValues.details.cost) : '' + ); // Manual fields - const [scanForMaintenance, setScanForMaintenance] = React.useState(false); + const [scanForMaintenance, setScanForMaintenance] = React.useState( + initialValues?.scanForMaintenance || false + ); + + // Shared vehicles for edit mode + const [selectedSharedVehicles, setSelectedSharedVehicles] = React.useState( + initialValues?.sharedVehicleIds || [] + ); const [file, setFile] = React.useState(null); const [uploadProgress, setUploadProgress] = React.useState(0); @@ -49,6 +89,9 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel const { data: vehicles } = useVehicles(); const create = useCreateDocument(); + const update = useUpdateDocument(documentId || ''); + const addSharedVehicle = useAddSharedVehicle(); + const removeSharedVehicle = useRemoveVehicleFromDocument(); const { hasAccess } = useTierAccess(); const canScanMaintenance = hasAccess('document.scanMaintenanceSchedule'); @@ -67,6 +110,7 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel setRegistrationExpirationDate(''); setRegistrationCost(''); setScanForMaintenance(false); + setSelectedSharedVehicles([]); setFile(null); setUploadProgress(0); setError(null); @@ -106,44 +150,100 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel } // Manual type: no details or dates, just scanForMaintenance flag - const created = await create.mutateAsync({ - vehicleId: vehicleID, - documentType: documentType, - title: title.trim(), - notes: notes.trim() || undefined, - details: Object.keys(details).length > 0 ? details : undefined, - issuedDate: issued_date, - expirationDate: expiration_date, - scanForMaintenance: documentType === 'manual' ? scanForMaintenance : undefined, - }); + if (mode === 'edit' && documentId) { + // Update existing document + await update.mutateAsync({ + title: title.trim(), + notes: notes.trim() || null, + details: Object.keys(details).length > 0 ? details : undefined, + issuedDate: issued_date || null, + expirationDate: expiration_date || null, + scanForMaintenance: documentType === 'manual' ? scanForMaintenance : undefined, + }); - if (file) { - const allowed = new Set(['application/pdf', 'image/jpeg', 'image/png']); - if (!file.type || !allowed.has(file.type)) { - setError('Unsupported file type. Allowed: PDF, JPG/JPEG, PNG.'); - return; + // Handle shared vehicles only for insurance documents + if (documentType === 'insurance') { + const currentSharedVehicleIds = initialValues?.sharedVehicleIds || []; + + // Add new shared vehicles + const vehiclesToAdd = selectedSharedVehicles.filter( + id => !currentSharedVehicleIds.includes(id) + ); + for (const vehicleId of vehiclesToAdd) { + await addSharedVehicle.mutateAsync({ docId: documentId, vehicleId }); + } + + // Remove unselected shared vehicles + const vehiclesToRemove = currentSharedVehicleIds.filter( + id => !selectedSharedVehicles.includes(id) + ); + for (const vehicleId of vehiclesToRemove) { + await removeSharedVehicle.mutateAsync({ docId: documentId, vehicleId }); + } } - try { - await documentsApi.uploadWithProgress(created.id, file, (pct) => setUploadProgress(pct)); - } catch (uploadErr: any) { - const status = uploadErr?.response?.status; - if (status === 415) { + + // Handle file upload if a new file was selected + if (file) { + const allowed = new Set(['application/pdf', 'image/jpeg', 'image/png']); + if (!file.type || !allowed.has(file.type)) { setError('Unsupported file type. Allowed: PDF, JPG/JPEG, PNG.'); return; } - setError(uploadErr?.message || 'Failed to upload file'); - return; + try { + await documentsApi.uploadWithProgress(documentId, file, (pct) => setUploadProgress(pct)); + } catch (uploadErr: any) { + const status = uploadErr?.response?.status; + if (status === 415) { + setError('Unsupported file type. Allowed: PDF, JPG/JPEG, PNG.'); + return; + } + setError(uploadErr?.message || 'Failed to upload file'); + return; + } } - } - resetForm(); - onSuccess?.(); + onSuccess?.(); + } else { + // Create new document + const created = await create.mutateAsync({ + vehicleId: vehicleID, + documentType: documentType, + title: title.trim(), + notes: notes.trim() || undefined, + details: Object.keys(details).length > 0 ? details : undefined, + issuedDate: issued_date, + expirationDate: expiration_date, + scanForMaintenance: documentType === 'manual' ? scanForMaintenance : undefined, + }); + + if (file) { + const allowed = new Set(['application/pdf', 'image/jpeg', 'image/png']); + if (!file.type || !allowed.has(file.type)) { + setError('Unsupported file type. Allowed: PDF, JPG/JPEG, PNG.'); + return; + } + try { + await documentsApi.uploadWithProgress(created.id, file, (pct) => setUploadProgress(pct)); + } catch (uploadErr: any) { + const status = uploadErr?.response?.status; + if (status === 415) { + setError('Unsupported file type. Allowed: PDF, JPG/JPEG, PNG.'); + return; + } + setError(uploadErr?.message || 'Failed to upload file'); + return; + } + } + + resetForm(); + onSuccess?.(); + } } catch (err: any) { const status = err?.response?.status; if (status === 415) { setError('Unsupported file type. Allowed: PDF, JPG/JPEG, PNG.'); } else { - setError(err?.message || 'Failed to create document'); + setError(err?.message || `Failed to ${mode === 'edit' ? 'update' : 'create'} document`); } } finally { setUploadProgress(0); @@ -159,6 +259,17 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel return v.id.slice(0, 8) + '...'; }; + // Filter out the primary vehicle from shared vehicle options + const sharedVehicleOptions = (vehicles || []).filter(v => v.id !== vehicleID); + + const handleSharedVehicleToggle = (vehicleId: string) => { + setSelectedSharedVehicles(prev => + prev.includes(vehicleId) + ? prev.filter(id => id !== vehicleId) + : [...prev, vehicleId] + ); + }; + return (
@@ -170,6 +281,7 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel value={vehicleID} onChange={(e) => setVehicleID(e.target.value)} required + disabled={mode === 'edit'} > {(vehicles || []).map((v: Vehicle) => ( @@ -184,6 +296,7 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:focus:ring-abudhabi dark:focus:border-abudhabi" value={documentType} onChange={(e) => setDocumentType(e.target.value as DocumentType)} + disabled={mode === 'edit'} > @@ -307,6 +420,32 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel onChange={(e) => setPremium(e.target.value)} /> + + {mode === 'edit' && sharedVehicleOptions.length > 0 && ( +
+ +
+ {sharedVehicleOptions.map((v) => ( + + ))} +
+
+ )} )} @@ -393,7 +532,9 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel
- +
= ({ onSuccess, onCancel )}
- +
diff --git a/frontend/src/features/documents/components/EditDocumentDialog.tsx b/frontend/src/features/documents/components/EditDocumentDialog.tsx new file mode 100644 index 0000000..76d9221 --- /dev/null +++ b/frontend/src/features/documents/components/EditDocumentDialog.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + IconButton, + useMediaQuery, + useTheme, +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { DocumentForm } from './DocumentForm'; +import type { DocumentRecord } from '../types/documents.types'; + +interface EditDocumentDialogProps { + open: boolean; + onClose: () => void; + document: DocumentRecord; +} + +export const EditDocumentDialog: React.FC = ({ + open, + onClose, + document, +}) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down('sm')); + + return ( + + {isSmall && ( + theme.palette.grey[500], + zIndex: 1, + }} + > + + + )} + + Edit Document + + + { + onClose(); + }} + onCancel={onClose} + /> + + + ); +}; + +export default EditDocumentDialog; diff --git a/frontend/src/features/documents/pages/DocumentDetailPage.tsx b/frontend/src/features/documents/pages/DocumentDetailPage.tsx index d8da13f..95977b4 100644 --- a/frontend/src/features/documents/pages/DocumentDetailPage.tsx +++ b/frontend/src/features/documents/pages/DocumentDetailPage.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useMemo } from 'react'; +import React, { useRef, useMemo, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useAuth0 } from '@auth0/auth0-react'; import { isAxiosError } from 'axios'; @@ -8,6 +8,7 @@ import { useDocument } from '../hooks/useDocuments'; import { useUploadWithProgress } from '../hooks/useUploadWithProgress'; import { documentsApi } from '../api/documents.api'; import { DocumentPreview } from '../components/DocumentPreview'; +import { EditDocumentDialog } from '../components/EditDocumentDialog'; import { useVehicle, useVehicles } from '../../vehicles/hooks/useVehicles'; import { getVehicleLabel } from '../utils/vehicleLabel'; @@ -15,11 +16,12 @@ export const DocumentDetailPage: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { isAuthenticated, isLoading: authLoading, loginWithRedirect } = useAuth0(); - const { data: doc, isLoading, error } = useDocument(id); + const { data: doc, isLoading, error, refetch } = useDocument(id); const { data: vehicle } = useVehicle(doc?.vehicleId || ''); const { data: vehicles } = useVehicles(); const inputRef = useRef(null); const upload = useUploadWithProgress(id!); + const [isEditOpen, setIsEditOpen] = useState(false); const vehiclesMap = useMemo(() => new Map(vehicles?.map(v => [v.id, v]) || []), [vehicles]); @@ -179,9 +181,10 @@ export const DocumentDetailPage: React.FC = () => {
-
+
+
{upload.isPending && (
Uploading... {upload.progress}%
@@ -195,6 +198,16 @@ export const DocumentDetailPage: React.FC = () => { )}
+ {doc && ( + { + setIsEditOpen(false); + refetch(); + }} + document={doc} + /> + )}
); };