feat(frontend): Dokumente-Upload-Widget (Modul 29 Frontend)
Backend war bereits fertig, das Frontend fehlte bislang. Neue gemeinsame DocumentUploadCard-Komponente (Liste hochgeladener Dateien, Upload per Dateiauswahl, Download, Löschen mit Bestätigungsdialog da unumkehrbar) plus dazugehöriges src/lib/documents/ API-Modul. apiFetch erzwingt bislang immer Content-Type: application/json, was multipart-Uploads verhindert hätte – daher neue apiFetchFormData- Hilfsfunktion in api-client.ts ergänzt (kein manuell gesetzter Content-Type, damit der Browser die multipart-Boundary selbst setzt). Eingebunden auf allen Detailseiten, die laut TASKS.md Modul 29 vorsehen und tatsächlich eine eigene Route haben: Bienenstand, Volk, Königin, Honigcharge. Durchsicht hat keine eigene Detailseite (nur Einträge in der Volk-Unterliste) und wird daher vorerst ausgelassen. Live verifiziert: Upload, Download und Löschen (inkl. Bestätigungsdialog) funktionieren End-to-End gegen das Backend, zero Konsolenfehler auf allen vier Detailseiten.
This commit is contained in:
1 parent
9a32c6c11d
commit
a21563cc76
9 files changed
+242
-1
No files matched your search
@@ -58,6 +58,41 @@ export async function apiFetchBlob(path: string, fallbackFilename: string): Prom
|
||||
return { blob, filename };
|
||||
}
|
||||
|
||||
async function rawFetchFormData(path: string, formData: FormData): Promise<Response> {
|
||||
const token = getAccessToken();
|
||||
return fetch(`${API_URL}${path}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function apiFetchFormData<T>(path: string, formData: FormData): Promise<T> {
|
||||
let response = await rawFetchFormData(path, formData);
|
||||
|
||||
if (response.status === 401) {
|
||||
const refreshed = await refreshRequest();
|
||||
if (refreshed) {
|
||||
setAccessToken(refreshed.accessToken);
|
||||
response = await rawFetchFormData(path, formData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const body: unknown = await response.json().catch(() => ({}));
|
||||
const message =
|
||||
typeof body === 'object' && body !== null && 'message' in body
|
||||
? String((body as { message: unknown }).message)
|
||||
: 'Request failed';
|
||||
throw new ApiError(response.status, message);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
let response = await rawFetch(path, init);
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { apiFetch, apiFetchBlob, apiFetchFormData } from "@/lib/auth/api-client";
|
||||
import { downloadBlob } from "@/lib/export/api";
|
||||
import { DocumentEntityType, DocumentSummary } from "./types";
|
||||
|
||||
export function fetchDocuments(
|
||||
entityType: DocumentEntityType,
|
||||
entityId: string,
|
||||
): Promise<DocumentSummary[]> {
|
||||
const params = new URLSearchParams({ entityType, entityId });
|
||||
return apiFetch<DocumentSummary[]>(`/documents?${params.toString()}`);
|
||||
}
|
||||
|
||||
export function uploadDocument(
|
||||
entityType: DocumentEntityType,
|
||||
entityId: string,
|
||||
file: File,
|
||||
): Promise<DocumentSummary> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("entityType", entityType);
|
||||
formData.append("entityId", entityId);
|
||||
return apiFetchFormData<DocumentSummary>("/documents", formData);
|
||||
}
|
||||
|
||||
export function deleteDocument(id: string): Promise<void> {
|
||||
return apiFetch<void>(`/documents/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export async function downloadDocument(id: string, fileName: string): Promise<void> {
|
||||
const { blob, filename } = await apiFetchBlob(`/documents/${id}/download`, fileName);
|
||||
downloadBlob(blob, filename);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export type DocumentEntityType = "APIARY" | "COLONY" | "QUEEN" | "INSPECTION" | "HONEY_BATCH";
|
||||
|
||||
export interface DocumentSummary {
|
||||
id: string;
|
||||
entityType: DocumentEntityType;
|
||||
entityId: string;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
sizeBytes: number;
|
||||
uploadedByUserId: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
Reference in new issue
Block a user