feat: Document feature enhancements (#31) #32

Merged
egullickson merged 7 commits from issue-31-document-enhancements into main 2026-01-15 02:35:56 +00:00
5 changed files with 142 additions and 0 deletions
Showing only changes of commit e558fdf8f9 - Show all commits

View File

@@ -47,5 +47,16 @@ export const documentsApi = {
// Return a blob for inline preview / download // Return a blob for inline preview / download
const res = await apiClient.get(`/documents/${id}/download`, { responseType: 'blob' }); const res = await apiClient.get(`/documents/${id}/download`, { responseType: 'blob' });
return res.data as Blob; return res.data as Blob;
},
async listByVehicle(vehicleId: string) {
const res = await apiClient.get<DocumentRecord[]>(`/documents/by-vehicle/${vehicleId}`);
return res.data;
},
async addSharedVehicle(docId: string, vehicleId: string) {
const res = await apiClient.post<DocumentRecord>(`/documents/${docId}/vehicles/${vehicleId}`);
return res.data;
},
async removeVehicleFromDocument(docId: string, vehicleId: string) {
await apiClient.delete(`/documents/${docId}/vehicles/${vehicleId}`);
} }
}; };

View File

@@ -30,6 +30,7 @@ describe('DocumentPreview', () => {
documentType: 'insurance', documentType: 'insurance',
title: 'Insurance Document', title: 'Insurance Document',
contentType: 'application/pdf', contentType: 'application/pdf',
sharedVehicleIds: [],
createdAt: '2024-01-01T00:00:00Z', createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z',
}; };
@@ -41,6 +42,7 @@ describe('DocumentPreview', () => {
documentType: 'registration', documentType: 'registration',
title: 'Registration Photo', title: 'Registration Photo',
contentType: 'image/jpeg', contentType: 'image/jpeg',
sharedVehicleIds: [],
createdAt: '2024-01-01T00:00:00Z', createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z',
}; };
@@ -52,6 +54,7 @@ describe('DocumentPreview', () => {
documentType: 'insurance', documentType: 'insurance',
title: 'Text Document', title: 'Text Document',
contentType: 'text/plain', contentType: 'text/plain',
sharedVehicleIds: [],
createdAt: '2024-01-01T00:00:00Z', createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z',
}; };

View File

@@ -50,6 +50,9 @@ export function useCreateDocument() {
fileHash: null, fileHash: null,
issuedDate: newDocument.issuedDate || null, issuedDate: newDocument.issuedDate || null,
expirationDate: newDocument.expirationDate || null, expirationDate: newDocument.expirationDate || null,
emailNotifications: newDocument.emailNotifications,
scanForMaintenance: newDocument.scanForMaintenance,
sharedVehicleIds: newDocument.sharedVehicleIds || [],
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
deletedAt: null, deletedAt: null,
@@ -225,3 +228,123 @@ export function useUploadDocument(id: string) {
networkMode: 'offlineFirst', networkMode: 'offlineFirst',
}); });
} }
export function useDocumentsByVehicle(vehicleId?: string) {
const query = useQuery({
queryKey: ['documents-by-vehicle', vehicleId],
queryFn: () => documentsApi.listByVehicle(vehicleId!),
enabled: !!vehicleId,
networkMode: 'offlineFirst',
});
return query;
}
export function useAddSharedVehicle() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ docId, vehicleId }: { docId: string; vehicleId: string }) =>
documentsApi.addSharedVehicle(docId, vehicleId),
onMutate: async ({ docId, vehicleId }) => {
// Cancel outgoing refetches
await qc.cancelQueries({ queryKey: ['document', docId] });
await qc.cancelQueries({ queryKey: ['documents'] });
await qc.cancelQueries({ queryKey: ['documents-by-vehicle'] });
// Snapshot previous values
const previousDocument = qc.getQueryData(['document', docId]);
const previousDocuments = qc.getQueryData(['documents']);
// Optimistically update individual document
qc.setQueryData(['document', docId], (old: DocumentRecord | undefined) => {
if (!old) return old;
return {
...old,
sharedVehicleIds: [...old.sharedVehicleIds, vehicleId],
updatedAt: new Date().toISOString(),
};
});
// Optimistically update documents list
qc.setQueryData(['documents'], (old: DocumentRecord[] | undefined) => {
if (!old) return old;
return old.map(doc =>
doc.id === docId
? { ...doc, sharedVehicleIds: [...doc.sharedVehicleIds, vehicleId], updatedAt: new Date().toISOString() }
: doc
);
});
return { previousDocument, previousDocuments };
},
onError: (_err, { docId }, context) => {
// Rollback on error
if (context?.previousDocument) {
qc.setQueryData(['document', docId], context.previousDocument);
}
if (context?.previousDocuments) {
qc.setQueryData(['documents'], context.previousDocuments);
}
},
onSettled: () => {
// Refetch to ensure consistency
qc.invalidateQueries({ queryKey: ['documents'] });
qc.invalidateQueries({ queryKey: ['documents-by-vehicle'] });
},
networkMode: 'offlineFirst',
});
}
export function useRemoveVehicleFromDocument() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ docId, vehicleId }: { docId: string; vehicleId: string }) =>
documentsApi.removeVehicleFromDocument(docId, vehicleId),
onMutate: async ({ docId, vehicleId }) => {
// Cancel outgoing refetches
await qc.cancelQueries({ queryKey: ['document', docId] });
await qc.cancelQueries({ queryKey: ['documents'] });
await qc.cancelQueries({ queryKey: ['documents-by-vehicle'] });
// Snapshot previous values
const previousDocument = qc.getQueryData(['document', docId]);
const previousDocuments = qc.getQueryData(['documents']);
// Optimistically update individual document
qc.setQueryData(['document', docId], (old: DocumentRecord | undefined) => {
if (!old) return old;
return {
...old,
sharedVehicleIds: old.sharedVehicleIds.filter(id => id !== vehicleId),
updatedAt: new Date().toISOString(),
};
});
// Optimistically update documents list
qc.setQueryData(['documents'], (old: DocumentRecord[] | undefined) => {
if (!old) return old;
return old.map(doc =>
doc.id === docId
? { ...doc, sharedVehicleIds: doc.sharedVehicleIds.filter(id => id !== vehicleId), updatedAt: new Date().toISOString() }
: doc
);
});
return { previousDocument, previousDocuments };
},
onError: (_err, { docId }, context) => {
// Rollback on error
if (context?.previousDocument) {
qc.setQueryData(['document', docId], context.previousDocument);
}
if (context?.previousDocuments) {
qc.setQueryData(['documents'], context.previousDocuments);
}
},
onSettled: () => {
// Refetch to ensure consistency
qc.invalidateQueries({ queryKey: ['documents'] });
qc.invalidateQueries({ queryKey: ['documents-by-vehicle'] });
},
networkMode: 'offlineFirst',
});
}

View File

@@ -31,6 +31,7 @@ describe('DocumentsMobileScreen', () => {
vehicleId: 'vehicle-1', vehicleId: 'vehicle-1',
documentType: 'insurance', documentType: 'insurance',
title: 'Car Insurance', title: 'Car Insurance',
sharedVehicleIds: [],
createdAt: '2024-01-01T00:00:00Z', createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z',
}, },
@@ -40,6 +41,7 @@ describe('DocumentsMobileScreen', () => {
vehicleId: 'vehicle-2', vehicleId: 'vehicle-2',
documentType: 'registration', documentType: 'registration',
title: 'Vehicle Registration', title: 'Vehicle Registration',
sharedVehicleIds: [],
createdAt: '2024-01-02T00:00:00Z', createdAt: '2024-01-02T00:00:00Z',
updatedAt: '2024-01-02T00:00:00Z', updatedAt: '2024-01-02T00:00:00Z',
}, },

View File

@@ -18,6 +18,7 @@ export interface DocumentRecord {
expirationDate?: string | null; expirationDate?: string | null;
emailNotifications?: boolean; emailNotifications?: boolean;
scanForMaintenance?: boolean; scanForMaintenance?: boolean;
sharedVehicleIds: string[];
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
deletedAt?: string | null; deletedAt?: string | null;
@@ -33,6 +34,7 @@ export interface CreateDocumentRequest {
expirationDate?: string; expirationDate?: string;
emailNotifications?: boolean; emailNotifications?: boolean;
scanForMaintenance?: boolean; scanForMaintenance?: boolean;
sharedVehicleIds?: string[];
} }
export interface UpdateDocumentRequest { export interface UpdateDocumentRequest {
@@ -43,5 +45,6 @@ export interface UpdateDocumentRequest {
expirationDate?: string | null; expirationDate?: string | null;
emailNotifications?: boolean; emailNotifications?: boolean;
scanForMaintenance?: boolean; scanForMaintenance?: boolean;
sharedVehicleIds?: string[];
} }