feat(verzeichnis): LDAP-Abgleich, Zeitplan, CSV-Import und Log (#30)
This commit was merged in pull request #30.
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user