"use client"; import * as React from "react"; import { useRouter } from "next/navigation"; import { AlertTriangle, CheckCircle2, FileSpreadsheet, Upload } from "lucide-react"; import { trpc } from "@/lib/trpc/client"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Fehlermeldung } from "@/components/stammdaten/anzeige"; import { cn } from "@/lib/utils"; /** * Import-Assistent. * * Drei Schritte, und der mittlere ist der eigentliche Punkt der Übung: * * 1. Datei wählen * 2. Zuordnung prüfen und Vorschau ansehen * 3. Übernehmen * * Zwischen 2 und 3 sitzt ein Mensch. Eine falsch zugeordnete Spalte verteilt * sonst Seriennummern auf Inventarnummern, und das merkt man erst Wochen * später am Regal. * * Die Datei wird beim Übernehmen erneut zum Server geschickt und dort neu * eingelesen. Das kostet eine Übertragung, verhindert aber, dass jemand die * Prüfung umgeht und eigene Werte einschleust. */ type ImportArt = "geraet" | "person" | "fahrzeug"; const ART_BEZEICHNUNG: Record = { geraet: "Geräte", person: "Mitglieder", fahrzeug: "Fahrzeuge", }; export function ImportAssistent() { const router = useRouter(); const werkzeuge = trpc.useUtils(); const [art, setArt] = React.useState("geraet"); const [datei, setDatei] = React.useState<{ name: string; inhalt: string } | null>(null); const [zuordnung, setZuordnung] = React.useState>({}); const [aktualisieren, setAktualisieren] = React.useState(false); const [fehler, setFehler] = React.useState(null); const [ergebnis, setErgebnis] = React.useState<{ angelegt: number; aktualisiert: number; uebersprungen: number; nichtUebernommen: number; fehler: string[]; } | null>(null); const vorschau = trpc.datenImport.vorschau.useMutation({ onSuccess: (daten) => { setZuordnung(daten.zuordnung); setFehler(null); }, onError: (ausnahme) => setFehler(ausnahme.message), }); const uebernehmen = trpc.datenImport.uebernehmen.useMutation({ onSuccess: (daten) => { setErgebnis(daten); setFehler(null); void werkzeuge.geraet.liste.invalidate(); void werkzeuge.organisation.kennzahlen.invalidate(); }, onError: (ausnahme) => setFehler(ausnahme.message), }); async function dateiWaehlen(ereignis: React.ChangeEvent) { const gewaehlt = ereignis.target.files?.[0]; if (!gewaehlt) return; setErgebnis(null); setFehler(null); const puffer = await gewaehlt.arrayBuffer(); // Blockweise umwandeln: bei großen Dateien sprengt ein einziger // String.fromCharCode-Aufruf den Aufrufstapel. const bytes = new Uint8Array(puffer); let roh = ""; const block = 8192; for (let i = 0; i < bytes.length; i += block) { roh += String.fromCharCode(...bytes.subarray(i, i + block)); } const inhalt = btoa(roh); setDatei({ name: gewaehlt.name, inhalt }); vorschau.mutate({ dateiname: gewaehlt.name, inhalt, art }); } function zuordnungAendern(spalte: string, feld: string) { const neu = { ...zuordnung, [spalte]: feld === "__keine__" ? null : feld }; setZuordnung(neu); if (datei) { vorschau.mutate({ dateiname: datei.name, inhalt: datei.inhalt, art, zuordnung: neu }); } } const daten = vorschau.data; // Schritt 3 geschafft. if (ergebnis) { return (
Import abgeschlossen Aus der Datei „{datei?.name}“.
{[ ["Neu angelegt", ergebnis.angelegt], ["Aktualisiert", ergebnis.aktualisiert], ["Übersprungen", ergebnis.uebersprungen], ["Nicht übernommen", ergebnis.nichtUebernommen], ].map(([bezeichnung, wert]) => (
{bezeichnung}
{wert}
))}
{ergebnis.uebersprungen > 0 && !aktualisieren && (

Übersprungen wurden Datensätze, die es schon gibt. Mit „Vorhandene aktualisieren“ werden sie stattdessen überschrieben.

)} {ergebnis.fehler.length > 0 && (

{ergebnis.fehler.length} Zeilen ließen sich nicht schreiben:

    {ergebnis.fehler.slice(0, 20).map((meldung) => (
  • {meldung}
  • ))}
)}
); } return (
{fehler && } {/* Schritt 1 */} 1. Datei wählen CSV oder Excel (.xlsx). Die erste Zeile muss die Spaltenüberschriften enthalten. Höchstens 10 MB und 5.000 Zeilen.
{datei && (

{datei.name}

)}
{vorschau.isPending && (

Die Datei wird gelesen …

)} {/* Schritt 2 */} {daten && ( <> 2. Spalten zuordnen Die Zuordnung wurde nach den Überschriften geraten. Bitte prüfen und nötigenfalls korrigieren — danach passt sich die Vorschau sofort an. {daten.fehlendePflichtfelder.length > 0 && (
Diesen Pflichtfeldern ist keine Spalte zugeordnet:{" "} {daten.fehlendePflichtfelder.join(", ")}. Ohne sie lässt sich nichts übernehmen.
)}
{daten.spalten.map((spalte) => (
))}
Vorschau {daten.anzahlUebernehmbar} von {daten.zeilenGesamt} Zeilen sind übernehmbar {daten.anzahlFehlerhaft > 0 && `, ${daten.anzahlFehlerhaft} haben Fehler`}. {daten.gekuerzt && " Die Datei wurde auf 5.000 Zeilen gekürzt."}
Zeile {daten.felder .filter((feld) => Object.values(zuordnung).includes(feld.schluessel), ) .map((feld) => ( {feld.bezeichnung} ))} Hinweis {daten.zeilen.map((zeile) => ( {zeile.nummer} {daten.felder .filter((feld) => Object.values(zuordnung).includes(feld.schluessel), ) .map((feld) => { const wert = zeile.werte[feld.schluessel]; return ( {wert instanceof Date ? wert.toLocaleDateString("de-DE") : wert === undefined || wert === null ? "—" : String(wert)} ); })} {zeile.fehler.length === 0 ? ( in Ordnung ) : ( {zeile.fehler.map((f) => f.meldung).join("; ")} )} ))}
{daten.anzahlFehlerhaft > 0 && (

Zeilen mit Fehlern werden übersprungen und bleiben in der Datei.

)}
)}
); }