"use client"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ColumnDef, flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table"; import { Copy, Download, Edit2, Eye, FilePlus2, GitBranchPlus, Search, Trash2, XCircle } from "lucide-react"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { useAuth } from "@/components/auth"; import { API_BASE, apiDelete, apiGet, apiSend, Customer, Device, Paginated, ValidationItem } from "@/lib/api"; const statusLabels: Record = { ENTWURF: "Entwurf", BEREIT_ZUR_PRUEFUNG: "Bereit zur Pruefung", IN_PRUEFUNG: "In Pruefung", FREIGEGEBEN: "Freigegeben", ABGESCHLOSSEN: "Abgeschlossen", STORNIERT: "Storniert" }; const statusClass: Record = { ENTWURF: "bg-border text-text-light", BEREIT_ZUR_PRUEFUNG: "bg-accent/35 text-primary-dark", IN_PRUEFUNG: "bg-warning/15 text-warning", FREIGEGEBEN: "bg-success/15 text-success", ABGESCHLOSSEN: "bg-primary-dark/15 text-primary-dark", STORNIERT: "bg-danger/15 text-danger" }; export default function ValidationsPage() { const { token } = useAuth(); const client = useQueryClient(); const [page, setPage] = useState(1); const [search, setSearch] = useState(""); const [status, setStatus] = useState(""); const [customerId, setCustomerId] = useState(""); const [deviceId, setDeviceId] = useState(""); const [validationType, setValidationType] = useState(""); const [result, setResult] = useState(""); const [dateFrom, setDateFrom] = useState(""); const [dateTo, setDateTo] = useState(""); const [overdueOnly, setOverdueOnly] = useState(false); const [sorting, setSorting] = useState([{ id: "updated_at", desc: true }]); const pageSize = 10; const sort = sorting[0] ?? { id: "updated_at", desc: true }; useEffect(() => { const initial = new URLSearchParams(window.location.search); setStatus(initial.get("status") ?? ""); setOverdueOnly(initial.get("overdue_only") === "true"); }, []); const params = new URLSearchParams({ page: String(page), page_size: String(pageSize), sort_by: sort.id, sort_order: sort.desc ? "desc" : "asc" }); if (search) params.set("search", search); if (status) params.set("status", status); if (customerId) params.set("customer_id", customerId); if (deviceId) params.set("device_id", deviceId); if (validationType) params.set("validation_type", validationType); if (result) params.set("result", result); if (dateFrom) params.set("date_from", dateFrom); if (dateTo) params.set("date_to", dateTo); if (overdueOnly) params.set("overdue_only", "true"); const validations = useQuery({ queryKey: ["validations", params.toString(), token], queryFn: () => apiGet>(`/validations?${params.toString()}`, token ?? ""), enabled: Boolean(token) }); const customers = useQuery({ queryKey: ["customers-options", token], queryFn: () => apiGet>("/customers?page=1&page_size=100", token ?? ""), enabled: Boolean(token) }); const devices = useQuery({ queryKey: ["devices-options", token], queryFn: () => apiGet>("/devices?page=1&page_size=100", token ?? ""), enabled: Boolean(token) }); const duplicateMutation = useMutation({ mutationFn: (id: string) => apiSend(`/validations/${id}/duplicate`, token ?? "", "POST", {}), onSuccess: () => client.invalidateQueries({ queryKey: ["validations"] }) }); const versionMutation = useMutation({ mutationFn: (id: string) => apiSend(`/validations/${id}/new-version`, token ?? "", "POST", {}), onSuccess: () => client.invalidateQueries({ queryKey: ["validations"] }) }); const cancelMutation = useMutation({ mutationFn: (id: string) => apiSend(`/validations/${id}/cancel`, token ?? "", "POST", {}), onSuccess: () => client.invalidateQueries({ queryKey: ["validations"] }) }); const deleteMutation = useMutation({ mutationFn: (id: string) => apiDelete(`/validations/${id}`, token ?? ""), onSuccess: () => client.invalidateQueries({ queryKey: ["validations"] }) }); async function downloadPdf(id: string, reportNumber: string) { const response = await fetch(`${API_BASE}/validations/${id}/report.pdf`, { headers: { Authorization: `Bearer ${token}` } }); const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${reportNumber}.pdf`; link.click(); URL.revokeObjectURL(url); } const columns = useMemo[]>(() => [ { accessorKey: "report_number", header: "Berichtsnummer" }, { accessorKey: "validation_type", header: "Art" }, { accessorKey: "performed_on", header: "Pruefdatum" }, { accessorKey: "next_validation_on", header: "Naechste Validierung" }, { accessorKey: "examiner_name", header: "Pruefer" }, { accessorKey: "result", header: "Ergebnis" }, { accessorKey: "updated_at", header: "Zuletzt geaendert" }, { accessorKey: "status", header: "Status", cell: ({ row }) => {statusLabels[row.original.status] ?? row.original.status} }, { id: "actions", header: "Aktionen", enableSorting: false, cell: ({ row }) => { const editable = ["ENTWURF", "BEREIT_ZUR_PRUEFUNG", "IN_PRUEFUNG"].includes(row.original.status); const versionable = ["FREIGEGEBEN", "ABGESCHLOSSEN"].includes(row.original.status); return
{editable && Weiterbearbeiten}{versionable && }{row.original.status === "ENTWURF" && }
; } } ], [cancelMutation, deleteMutation, duplicateMutation, token]); const table = useReactTable({ data: validations.data?.items ?? [], columns, state: { sorting }, manualSorting: true, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel() }); const totalPages = Math.max(1, Math.ceil((validations.data?.total ?? 0) / pageSize)); return (

Validierungen

Verwaltung, Vorschau, Export und Workflow.

Neue Validierung
setDateFrom(event.target.value)} className="h-12 rounded-lg border border-border px-3" /> setDateTo(event.target.value)} className="h-12 rounded-lg border border-border px-3" />
{table.getHeaderGroups().map((group) => {group.headers.map((header) => )})}{table.getRowModel().rows.map((row) => {row.getVisibleCells().map((cell) => )})}
{flexRender(header.column.columnDef.header, header.getContext())}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{validations.data?.total ?? 0} Datensaetze
Seite {page} von {totalPages}
); } type ImportPreview = { rows: { row_number: number; data: Record; errors: string[]; duplicate: boolean }[]; valid_rows: number; invalid_rows: number; duplicates: number; }; function ImportPanel() { const { token } = useAuth(); const client = useQueryClient(); const [preview, setPreview] = useState(null); const [summary, setSummary] = useState(""); const [strategy, setStrategy] = useState("skip"); async function previewCsv(file: File | null) { if (!file || !token) return; const formData = new FormData(); formData.append("file", file); const response = await fetch(`${API_BASE}/validations/import/csv-preview`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: formData }); setPreview(await response.json()); } async function importJson(file: File | null) { if (!file || !token) return; const parsed = JSON.parse(await file.text()); const rows = Array.isArray(parsed) ? parsed : [parsed]; const response = await fetch(`${API_BASE}/validations/import/json`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ rows, duplicate_strategy: strategy }) }); const result = await response.json(); setSummary(`Erfolgreich: ${result.successful}, uebersprungen: ${result.skipped}, fehlerhaft: ${result.failed}`); client.invalidateQueries({ queryKey: ["validations"] }); } return (

Import

CSV-Vorschau und JSON-Import mit Duplikatstrategie.

{summary &&

{summary}

} {preview &&

Gueltig: {preview.valid_rows}, fehlerhaft: {preview.invalid_rows}, Duplikate: {preview.duplicates}

{preview.rows.map((row) => )}
ZeileBerichtsnummerStatusFehler
{row.row_number}{row.data.report_number}{row.duplicate ? "Duplikat" : "Neu"}{row.errors.join(", ")}
}
); }