159 lines
6.3 KiB
TypeScript
159 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import { useActionState, useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { FELD } from "@/lib/formular";
|
|
import { mitarbeiterImportierenAktion, type AktionsErgebnis } from "@/server/actions/mitarbeiter";
|
|
|
|
/**
|
|
* Personen aus einer CSV-Datei einspielen (#016).
|
|
*
|
|
* Die Datei wird **im Browser** gelesen und ihr Inhalt als Text mitgeschickt —
|
|
* derselbe Weg wie beim Geraete- und Vorlagenimport, und dieselbe Begruendung:
|
|
* das Format ist Text, die Dateien sind klein, und so gibt es einen Weg statt
|
|
* zwei.
|
|
*
|
|
* Was ueber dem Feld steht, muss der Dienst auch halten. Deshalb stehen dort nur
|
|
* Zusagen, die in `mitarbeiterImportieren` als Code nachzulesen sind: die
|
|
* Obergrenze, die Spalten, die Wiedererkennung, und dass eine leere Zelle nichts
|
|
* loescht.
|
|
*/
|
|
export function MitarbeiterImportieren({
|
|
hoechstZeilen,
|
|
spalten,
|
|
kunden,
|
|
}: {
|
|
hoechstZeilen: number;
|
|
spalten: readonly string[];
|
|
kunden: readonly { readonly id: string; readonly name: string }[];
|
|
}) {
|
|
const [ergebnis, aktion, laeuft] = useActionState<AktionsErgebnis, FormData>(
|
|
mitarbeiterImportierenAktion,
|
|
{},
|
|
);
|
|
const [inhalt, setInhalt] = useState("");
|
|
const [leseFehler, setLeseFehler] = useState<string | null>(null);
|
|
|
|
async function dateiLesen(datei: File | undefined) {
|
|
if (!datei) return;
|
|
setLeseFehler(null);
|
|
try {
|
|
setInhalt(await datei.text());
|
|
} catch {
|
|
setLeseFehler("Die Datei ließ sich nicht lesen.");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form action={aktion} className="flex flex-col gap-4">
|
|
<p className="max-w-prose text-sm text-nebentext">
|
|
Spalten: <span className="mono text-text">{spalten.join(", ")}</span>. Die deutschen Namen (Vorname,
|
|
Nachname, Anzeigename, Abteilung, Titel) gehen genauso, Groß- und Kleinschreibung ist unerheblich.
|
|
Trennzeichen darf Komma oder Semikolon sein. Höchstens {hoechstZeilen} Zeilen je Datei. Eine
|
|
fehlerhafte Zeile bricht den Import nicht ab — sie wird unten mit ihrer Zeilennummer gemeldet.
|
|
</p>
|
|
<ul className="max-w-prose list-disc pl-5 text-sm text-nebentext">
|
|
<li>
|
|
Wiedererkannt wird eine Person über <span className="mono text-text">sam</span> oder{" "}
|
|
<span className="mono text-text">upn</span>, sonst über den Anzeigenamen. Wer schon da ist, wird
|
|
aktualisiert statt ein zweites Mal angelegt.
|
|
</li>
|
|
<li>
|
|
Eine leere Zelle löscht nichts: was in der Datei nicht steht, bleibt so, wie es ist. Fehlen{" "}
|
|
<span className="mono text-text">givenName</span> und <span className="mono text-text">sn</span>,
|
|
wird <span className="mono text-text">displayName</span> am letzten Leerzeichen geteilt.
|
|
</li>
|
|
<li>
|
|
Personen aus dem Verzeichnis werden nicht überschrieben; solche Zeilen werden gemeldet. Was hier
|
|
entsteht, gilt als von Hand angelegt und bleibt änderbar.
|
|
</li>
|
|
<li>
|
|
Eine deaktivierte Person wird erkannt und aktualisiert, aber <strong>nicht</strong> wieder aktiviert
|
|
— dafür gibt es den Schalter auf ihrer Seite.
|
|
</li>
|
|
<li>
|
|
Abteilungen legt der Import nicht an — der Name wird nur bei der Person eingetragen. Als Abteilung
|
|
mit Ziel-OU und Standardgruppen übernimmt sie ein Administrator.
|
|
</li>
|
|
</ul>
|
|
|
|
<label className="flex flex-col gap-1.5 text-sm">
|
|
<span className="text-nebentext">CSV-Datei</span>
|
|
<input
|
|
type="file"
|
|
accept="text/csv,.csv,text/plain,.txt"
|
|
onChange={(ereignis) => void dateiLesen(ereignis.target.files?.[0])}
|
|
className="min-h-11 rounded-karte border border-linie bg-grund px-3 py-2 text-sm text-nebentext file:mr-3 file:rounded-pille file:border-0 file:bg-flaeche-hoch file:px-3 file:py-1.5 file:text-sm file:text-text"
|
|
/>
|
|
</label>
|
|
<label className="flex flex-col gap-1.5 text-sm">
|
|
<span className="text-nebentext">… oder Inhalt einfügen</span>
|
|
<textarea
|
|
name="inhalt"
|
|
rows={6}
|
|
required
|
|
value={inhalt}
|
|
onChange={(ereignis) => setInhalt(ereignis.target.value)}
|
|
className="mono rounded-karte border border-linie bg-grund px-3 py-2 text-xs text-text outline-none focus-visible:border-akzent"
|
|
placeholder={
|
|
"givenName;sn;displayName;mail;upn;sam;department;title\n" +
|
|
"Anna;Muster;Anna Muster;anna.muster@firma.de;anna.muster@firma.de;amuster;Vertrieb;Innendienst"
|
|
}
|
|
/>
|
|
</label>
|
|
|
|
{kunden.length > 0 ? (
|
|
<label className="flex flex-col gap-1.5 text-sm">
|
|
<span className="text-nebentext">Kunde</span>
|
|
<select name="kundeId" defaultValue="" className={FELD} required>
|
|
<option value="">bitte wählen</option>
|
|
{kunden.map((kunde) => (
|
|
<option key={kunde.id} value={kunde.id}>
|
|
{kunde.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
) : null}
|
|
|
|
{leseFehler ? (
|
|
<p role="alert" className="text-sm text-fehler">
|
|
{leseFehler}
|
|
</p>
|
|
) : null}
|
|
{ergebnis.fehler ? (
|
|
<p role="alert" className="text-sm text-fehler">
|
|
{ergebnis.fehler}
|
|
</p>
|
|
) : null}
|
|
{ergebnis.erfolg ? (
|
|
<p role="status" className="text-sm text-erledigt">
|
|
{ergebnis.erfolg}
|
|
</p>
|
|
) : null}
|
|
|
|
{/* Je abgewiesene Zeile die Nummer, die der Editor des Benutzers anzeigt,
|
|
und der Grund. Eine Sammelmeldung waere hier nutzlos: die Datei muss an
|
|
genau diesen Zeilen geaendert werden. */}
|
|
{ergebnis.abgewiesen && ergebnis.abgewiesen.length > 0 ? (
|
|
<ul className="flex flex-col gap-1.5 rounded-karte border border-linie bg-grund p-3 text-xs">
|
|
{ergebnis.abgewiesen.map((abweisung) => (
|
|
<li key={abweisung.zeile} className="flex flex-wrap gap-2">
|
|
<span className="mono shrink-0 text-nebentext">Zeile {abweisung.zeile}</span>
|
|
<span className="min-w-0 text-fehler">{abweisung.grund}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : null}
|
|
|
|
<Button
|
|
type="submit"
|
|
disabled={laeuft}
|
|
className="h-11 self-start rounded-pille px-5 text-sm font-semibold"
|
|
>
|
|
{laeuft ? "Wird eingelesen …" : "Einlesen"}
|
|
</Button>
|
|
</form>
|
|
);
|
|
}
|