Add document storage (local filesystem) + fix dashboard outstanding KPI

- Local file storage via STORAGE_PATH env var (replaces DO Spaces)
- POST/GET/DELETE /api/cases/:caseId/documents + GET /api/documents/:docId/download
- useDocuments hook + upload/list/download/delete UI in CaseDetailPage
- GET /api/invoices/summary — live SUM of sent+overdue invoices
- Dashboard outstanding KPI wired to real DB value
- api.upload() for FormData, formatBytes() utility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Leon Serfaty
2026-04-26 03:07:37 -04:00
co-authored by Claude Sonnet 4.6
parent 0700d54225
commit ce2278f4be
13 changed files with 334 additions and 14 deletions
+40
View File
@@ -0,0 +1,40 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { api, type ApiError } from '@/lib/api';
export interface Document {
id: string;
name: string;
mimeType: string;
sizeBytes: number;
createdAt: string;
}
const KEY = (caseId: string) => ['documents', caseId] as const;
export function useDocuments(caseId: string | undefined) {
return useQuery<Document[]>({
queryKey: caseId ? KEY(caseId) : ['documents', 'noop'],
queryFn: () => api.get(`/api/cases/${caseId}/documents`),
enabled: !!caseId,
});
}
export function useUploadDocument(caseId: string) {
const qc = useQueryClient();
return useMutation<Document, ApiError, File>({
mutationFn: (file) => {
const form = new FormData();
form.append('file', file);
return api.upload(`/api/cases/${caseId}/documents`, form);
},
onSuccess: () => qc.invalidateQueries({ queryKey: KEY(caseId) }),
});
}
export function useDeleteDocument(caseId: string) {
const qc = useQueryClient();
return useMutation<void, ApiError, string>({
mutationFn: (docId) => api.delete(`/api/documents/${docId}`),
onSuccess: () => qc.invalidateQueries({ queryKey: KEY(caseId) }),
});
}
+7
View File
@@ -90,6 +90,13 @@ function qs(p: ListParams): string {
return s ? `?${s}` : '';
}
export function useInvoiceSummary() {
return useQuery<{ outstanding: string }>({
queryKey: ['invoices', 'summary'],
queryFn: () => api.get('/api/invoices/summary'),
});
}
export function useInvoices(params: ListParams = {}) {
return useQuery<ListResponse>({
queryKey: KEY.list(params),