diff --git a/ApiCore-Frontend/src/app/(app)/settings/import/page.tsx b/ApiCore-Frontend/src/app/(app)/settings/import/page.tsx new file mode 100644 index 0000000..6fcd2f9 --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/settings/import/page.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { ChangeEvent, useState } from "react"; +import { Upload } from "lucide-react"; +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { importCsv } from "@/lib/import/api"; +import { ImportEntity, ImportReport, ImportRowStatus } from "@/lib/import/types"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { PageHeader } from "@/components/page-header"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +const ENTITY_LABELS: Record = { + apiaries: "Bienenstände", + colonies: "Völker", + queens: "Königinnen", +}; + +const ENTITY_COLUMNS: Record = { + apiaries: "name, description, location, latitude, longitude, address, notes", + colonies: + "code, apiaryName, colonyType, hiveType, originText, strength, broodStatus, feedStatus, healthStatus, notes", + queens: + "code, hatchYear, breed, origin, breeder, markingColor, markingNumber, matingType, matingStatus, inseminationData, motherLine, fatherLine, notes", +}; + +const ROW_STATUS_VARIANT: Record = { + created: "default", + skipped: "secondary", + error: "destructive", +}; + +const ROW_STATUS_LABELS: Record = { + created: "Erstellt", + skipped: "Übersprungen", + error: "Fehler", +}; + +export default function ImportPage() { + const [entity, setEntity] = useState("apiaries"); + const [file, setFile] = useState(null); + const [report, setReport] = useState(null); + + const mutation = useMutation({ + mutationFn: () => { + if (!file) { + throw new Error("Keine Datei ausgewählt"); + } + return importCsv(entity, file); + }, + onSuccess: (result) => { + setReport(result); + toast.success( + `Import abgeschlossen: ${result.created} erstellt, ${result.skipped} übersprungen, ${result.errors} Fehler`, + ); + }, + onError: () => toast.error("Import fehlgeschlagen"), + }); + + function handleFileChange(event: ChangeEvent) { + setFile(event.target.files?.[0] ?? null); + } + + function handleSubmit() { + if (!file) { + toast.error("Bitte eine CSV-Datei auswählen"); + return; + } + mutation.mutate(); + } + + return ( +
+ + + + + Datei hochladen + + +
+ + +

Spalten: {ENTITY_COLUMNS[entity]}

+
+
+ + +
+ +
+
+ + {report ? ( + + + Ergebnisbericht ({ENTITY_LABELS[report.entity]}) +
+ {report.created} erstellt + {report.skipped} übersprungen + {report.errors} Fehler +
+
+ +
+ + + + Zeile + Status + Meldung + + + + {report.rows.map((row) => ( + + {row.row} + + + {ROW_STATUS_LABELS[row.status]} + + + + {row.message ?? "—"} + + + ))} + +
+
+
+
+ ) : null} +
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/sidebar.tsx b/ApiCore-Frontend/src/app/(app)/sidebar.tsx index c5d0bfc..d5be5f1 100644 --- a/ApiCore-Frontend/src/app/(app)/sidebar.tsx +++ b/ApiCore-Frontend/src/app/(app)/sidebar.tsx @@ -18,6 +18,7 @@ import { Package, Search, ShoppingBag, + Upload, Users, Warehouse, Webhook, @@ -84,6 +85,7 @@ const ADMIN_GROUP: NavGroup = { { label: "Audit-Log", href: "/settings/audit-log", icon: History }, { label: "Webhooks", href: "/settings/webhooks", icon: Webhook }, { label: "Automatisierungen", href: "/settings/automation-rules", icon: Zap }, + { label: "Import", href: "/settings/import", icon: Upload }, ], }; diff --git a/ApiCore-Frontend/src/lib/import/api.ts b/ApiCore-Frontend/src/lib/import/api.ts new file mode 100644 index 0000000..d5e063a --- /dev/null +++ b/ApiCore-Frontend/src/lib/import/api.ts @@ -0,0 +1,8 @@ +import { apiFetchFormData } from "@/lib/auth/api-client"; +import { ImportEntity, ImportReport } from "./types"; + +export function importCsv(entity: ImportEntity, file: File): Promise { + const formData = new FormData(); + formData.append("file", file); + return apiFetchFormData(`/import/${entity}`, formData); +} diff --git a/ApiCore-Frontend/src/lib/import/types.ts b/ApiCore-Frontend/src/lib/import/types.ts new file mode 100644 index 0000000..18a7613 --- /dev/null +++ b/ApiCore-Frontend/src/lib/import/types.ts @@ -0,0 +1,18 @@ +export type ImportEntity = "apiaries" | "colonies" | "queens"; +export type ImportRowStatus = "created" | "skipped" | "error"; + +export interface ImportRowResult { + row: number; + status: ImportRowStatus; + message?: string; + id?: string; +} + +export interface ImportReport { + entity: ImportEntity; + totalRows: number; + created: number; + skipped: number; + errors: number; + rows: ImportRowResult[]; +}