feat: Document feature enhancements (#31) #32
@@ -47,5 +47,16 @@ export const documentsApi = {
|
||||
// Return a blob for inline preview / download
|
||||
const res = await apiClient.get(`/documents/${id}/download`, { responseType: '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}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('DocumentPreview', () => {
|
||||
documentType: 'insurance',
|
||||
title: 'Insurance Document',
|
||||
contentType: 'application/pdf',
|
||||
sharedVehicleIds: [],
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
@@ -41,6 +42,7 @@ describe('DocumentPreview', () => {
|
||||
documentType: 'registration',
|
||||
title: 'Registration Photo',
|
||||
contentType: 'image/jpeg',
|
||||
sharedVehicleIds: [],
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
@@ -52,6 +54,7 @@ describe('DocumentPreview', () => {
|
||||
documentType: 'insurance',
|
||||
title: 'Text Document',
|
||||
contentType: 'text/plain',
|
||||
sharedVehicleIds: [],
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
@@ -50,6 +50,9 @@ export function useCreateDocument() {
|
||||
fileHash: null,
|
||||
issuedDate: newDocument.issuedDate || null,
|
||||
expirationDate: newDocument.expirationDate || null,
|
||||
emailNotifications: newDocument.emailNotifications,
|
||||
scanForMaintenance: newDocument.scanForMaintenance,
|
||||
sharedVehicleIds: newDocument.sharedVehicleIds || [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
@@ -225,3 +228,123 @@ export function useUploadDocument(id: string) {
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ describe('DocumentsMobileScreen', () => {
|
||||
vehicleId: 'vehicle-1',
|
||||
documentType: 'insurance',
|
||||
title: 'Car Insurance',
|
||||
sharedVehicleIds: [],
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
@@ -40,6 +41,7 @@ describe('DocumentsMobileScreen', () => {
|
||||
vehicleId: 'vehicle-2',
|
||||
documentType: 'registration',
|
||||
title: 'Vehicle Registration',
|
||||
sharedVehicleIds: [],
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
updatedAt: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface DocumentRecord {
|
||||
expirationDate?: string | null;
|
||||
emailNotifications?: boolean;
|
||||
scanForMaintenance?: boolean;
|
||||
sharedVehicleIds: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string | null;
|
||||
@@ -33,6 +34,7 @@ export interface CreateDocumentRequest {
|
||||
expirationDate?: string;
|
||||
emailNotifications?: boolean;
|
||||
scanForMaintenance?: boolean;
|
||||
sharedVehicleIds?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateDocumentRequest {
|
||||
@@ -43,5 +45,6 @@ export interface UpdateDocumentRequest {
|
||||
expirationDate?: string | null;
|
||||
emailNotifications?: boolean;
|
||||
scanForMaintenance?: boolean;
|
||||
sharedVehicleIds?: string[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user