diff --git a/ApiCore-Frontend/src/app/(app)/apiaries/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/apiaries/[id]/page.tsx index 3ed7324..961dbb1 100644 --- a/ApiCore-Frontend/src/app/(app)/apiaries/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/apiaries/[id]/page.tsx @@ -5,6 +5,7 @@ import { useParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import { fetchApiary } from "@/lib/apiaries/api"; import { QrCodeDialog } from "@/components/qr-code-dialog"; +import { DocumentUploadCard } from "@/components/document-upload-card"; import { Button } from "@/components/ui/button"; import { ApiaryEditCard } from "./apiary-edit-card"; @@ -32,6 +33,7 @@ export default function ApiaryDetailPage() { + + + + + {isLoading ?

Lädt…

: null} + {!isLoading && (!documents || documents.length === 0) ? ( + + ) : null} + {documents?.map((document) => ( +
+
+ +
+

{document.fileName}

+

+ {formatFileSize(document.sizeBytes)} ·{" "} + {new Date(document.createdAt).toLocaleDateString("de-DE")} +

+
+
+
+ + deleteMutation.mutate(document.id)} + /> +
+
+ ))} +
+ + ); +} diff --git a/ApiCore-Frontend/src/lib/auth/api-client.ts b/ApiCore-Frontend/src/lib/auth/api-client.ts index e5247ee..cb2ceef 100644 --- a/ApiCore-Frontend/src/lib/auth/api-client.ts +++ b/ApiCore-Frontend/src/lib/auth/api-client.ts @@ -58,6 +58,41 @@ export async function apiFetchBlob(path: string, fallbackFilename: string): Prom return { blob, filename }; } +async function rawFetchFormData(path: string, formData: FormData): Promise { + const token = getAccessToken(); + return fetch(`${API_URL}${path}`, { + method: 'POST', + body: formData, + credentials: 'include', + headers: { + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + }); +} + +export async function apiFetchFormData(path: string, formData: FormData): Promise { + 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; +} + export async function apiFetch(path: string, init: RequestInit = {}): Promise { let response = await rawFetch(path, init); diff --git a/ApiCore-Frontend/src/lib/documents/api.ts b/ApiCore-Frontend/src/lib/documents/api.ts new file mode 100644 index 0000000..05b4505 --- /dev/null +++ b/ApiCore-Frontend/src/lib/documents/api.ts @@ -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 { + const params = new URLSearchParams({ entityType, entityId }); + return apiFetch(`/documents?${params.toString()}`); +} + +export function uploadDocument( + entityType: DocumentEntityType, + entityId: string, + file: File, +): Promise { + const formData = new FormData(); + formData.append("file", file); + formData.append("entityType", entityType); + formData.append("entityId", entityId); + return apiFetchFormData("/documents", formData); +} + +export function deleteDocument(id: string): Promise { + return apiFetch(`/documents/${id}`, { method: "DELETE" }); +} + +export async function downloadDocument(id: string, fileName: string): Promise { + const { blob, filename } = await apiFetchBlob(`/documents/${id}/download`, fileName); + downloadBlob(blob, filename); +} diff --git a/ApiCore-Frontend/src/lib/documents/types.ts b/ApiCore-Frontend/src/lib/documents/types.ts new file mode 100644 index 0000000..e1e520a --- /dev/null +++ b/ApiCore-Frontend/src/lib/documents/types.ts @@ -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; +} diff --git a/TASKS.md b/TASKS.md index a6c875c..aff4970 100644 --- a/TASKS.md +++ b/TASKS.md @@ -171,7 +171,7 @@ Reihenfolge = Bau-Reihenfolge. Gleiche Haken-Regel wie MVP: erst setzen, wenn Li - [x] Design-Tokens: warme Honig-/Wachs-Farbpalette (Amber-Primär, Salbeigrün-Sekundär, Warnung/Erfolg), Space Grotesk (Überschrift) + Inter (Fließtext) + IBM Plex Mono (Codes/IDs) - [x] Navigation, Dashboard, Login neu gestaltet (Hexagon-Markenzeichen, aktiver Link-Zustand, Waben-Signaturmuster) -- [ ] Verbleibende Bestandsseiten (Listen/Details aller MVP-Module) auf einheitlichen Karten-/Badge-/Icon-Stil durchgezogen +- [x] Verbleibende Bestandsseiten (Listen/Details aller MVP-Module) auf einheitlichen Karten-/Badge-/Icon-Stil durchgezogen - [ ] Alle neuen v1.x-Module folgen dem Redesign von Anfang an (kein Nacharbeiten nötig) ### 22. QR-Code-System