Files
Laufzettel/src/components/mitarbeiter-stammdaten.tsx
T
Florian WildeandClaude Opus 5 86d55917f9
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(stammdaten): Mitarbeiter, Abteilungen und Geraete
Die Stammdaten, an denen die Historie haengt. Personen werden deaktiviert,
Geraete und Abteilungen archiviert — geloescht wird nichts, woran ein Lauf
haengen kann.

- Mitarbeiterliste mit Suche ueber Anzeigename, Mail, UPN und sAMAccountName,
  Filter nach Abteilung, standardmaessig nur aktive; die Suche ist ein eigenes
  Bauteil, weil #013 und #014 sie wieder brauchen
- Personenseite mit Stammdaten, Geraeten, Vorgesetztem; was aus dem Verzeichnis
  kommt, ist gesperrt — der naechste Abgleich wuerde es ohnehin zurueckdrehen
- Abteilungen unter Admin: Ziel-OU, Standardgruppen, Mail-Domain, dazu die
  Namen, die bei Personen vorkommen, aber nicht gepflegt sind
- Geraete mit Suche ueber Hostname, Bezeichnung, Seriennummer und Tag-Code;
  CSV-Import mit einem Ergebnis je Zeile
- Neues Recht mitarbeiterBearbeiten; die Seitenleiste fuehrt Personen und
  Geraete unter einem Eintrag, im Modus Dienstleistung anders beschriftet

Der unabhaengige Review fand zwei Blocker. Der schwerere meldete Erfolg: ein
nicht geschlossenes Anfuehrungszeichen verschluckte im CSV-Import alle
Folgezeilen bis zum naechsten `"`, und der Import sagte „3 von 3 Zeilen
angelegt". Ein Wert reicht jetzt nicht mehr ueber die Zeile hinaus — fuer
Hostnamen und Seriennummern ist ein Umbruch praktisch immer ein vergessenes
Anfuehrungszeichen, und so endet der Schaden an seiner Zeile. Der zweite:
ungepflegte Abteilungen wurden ueber alle Kunden hinweg gruppiert.

Dazu sieben weitere Befunde, alle behoben — darunter eine Besitzerauswahl, die
Personen fremder Kunden anbot, und ein Techniker, der ein Geraet endgueltig
loeschen konnte.

610 Tests gruen, 18 E2E-Ablaeufe gegen den gebauten Stand.

Schliesst #011

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 12:51:54 +02:00

154 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useActionState } from "react";
import { Lock } from "lucide-react";
import { Button } from "@/components/ui/button";
import { mitarbeiterBearbeitenAktion, type AktionsErgebnis } from "@/server/actions/mitarbeiter";
import { FELD } from "@/lib/formular";
export type StammdatenPerson = {
readonly id: string;
readonly vorname: string;
readonly nachname: string;
readonly anzeigename: string;
readonly mail: string | null;
readonly upn: string | null;
readonly sam: string | null;
readonly abteilung: string | null;
readonly titel: string | null;
/** Bei `true` kommen die Stammdaten aus dem Verzeichnis und sind gesperrt. */
readonly ausVerzeichnis: boolean;
};
/**
* Stammdaten bearbeiten – oder ansehen, wenn sie aus dem Verzeichnis kommen.
*
* Bei `herkunft = ABGLEICH` ist **jedes** gepflegte Feld gesperrt, nicht nur
* einige: alles daran wird im Verzeichnis gepflegt, und der naechste Abgleich
* wuerde eine Aenderung ohnehin zurueckdrehen. Die Felder werden trotzdem
* gezeigt — man will die Werte sehen —, nur eben schreibgeschuetzt. Ein
* ausgeblendetes Feld waere eine schlechtere Auskunft als ein gesperrtes.
*/
export function MitarbeiterStammdaten({
person,
darfBearbeiten,
}: {
person: StammdatenPerson;
darfBearbeiten: boolean;
}) {
const [ergebnis, aktion, laeuft] = useActionState<AktionsErgebnis, FormData>(
mitarbeiterBearbeitenAktion,
{},
);
/*
* Zwei verschiedene Gruende, dasselbe Ergebnis — aber nicht dieselbe Meldung.
* Wer die Rolle nicht hat, bekommt keinen Satz ueber das Verzeichnis zu lesen,
* der fuer ihn gar nicht der Grund ist.
*/
const gesperrt = person.ausVerzeichnis || !darfBearbeiten;
return (
<form action={aktion} className="flex flex-col gap-4">
<input type="hidden" name="mitarbeiterId" value={person.id} />
{gesperrt ? (
<div className="flex items-start gap-3 rounded-karte border border-linie bg-flaeche-hoch p-4">
<Lock
className="mt-0.5 size-[18px] shrink-0 text-nebentext"
strokeWidth={1.75}
aria-hidden="true"
/>
<p className="max-w-prose text-sm text-nebentext">
{person.ausVerzeichnis
? "Diese Person kommt aus dem Verzeichnis. Die Stammdaten werden dort gepflegt — eine Änderung hier würde der nächste Abgleich zurücksetzen. Deaktivieren lässt sie sich trotzdem."
: "Zum Ändern der Stammdaten fehlt dir die Berechtigung. Dafür braucht es die Rolle Techniker, Redakteur oder Administrator."}
</p>
</div>
) : null}
<div className="grid gap-4 sm:grid-cols-2">
<label className="flex flex-col gap-1.5 text-sm">
<span className="text-nebentext">Vorname</span>
<input name="vorname" required defaultValue={person.vorname} readOnly={gesperrt} className={FELD} />
</label>
<label className="flex flex-col gap-1.5 text-sm">
<span className="text-nebentext">Nachname</span>
<input
name="nachname"
required
defaultValue={person.nachname}
readOnly={gesperrt}
className={FELD}
/>
</label>
</div>
<label className="flex flex-col gap-1.5 text-sm">
<span className="text-nebentext">Anzeigename</span>
<input name="anzeigename" defaultValue={person.anzeigename} readOnly={gesperrt} className={FELD} />
</label>
<div className="grid gap-4 sm:grid-cols-2">
<label className="flex flex-col gap-1.5 text-sm">
<span className="text-nebentext">Mail-Adresse</span>
<input
name="mail"
type="email"
defaultValue={person.mail ?? ""}
readOnly={gesperrt}
className={FELD}
/>
</label>
<label className="flex flex-col gap-1.5 text-sm">
<span className="text-nebentext">Abteilung</span>
<input
name="abteilung"
defaultValue={person.abteilung ?? ""}
readOnly={gesperrt}
className={FELD}
/>
</label>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<label className="flex flex-col gap-1.5 text-sm">
<span className="text-nebentext">UPN</span>
<input name="upn" defaultValue={person.upn ?? ""} readOnly={gesperrt} className={FELD} />
</label>
<label className="flex flex-col gap-1.5 text-sm">
<span className="text-nebentext">sAMAccountName</span>
<input
name="sam"
maxLength={20}
defaultValue={person.sam ?? ""}
readOnly={gesperrt}
className={`${FELD} mono`}
/>
</label>
<label className="flex flex-col gap-1.5 text-sm">
<span className="text-nebentext">Titel</span>
<input name="titel" defaultValue={person.titel ?? ""} readOnly={gesperrt} className={FELD} />
</label>
</div>
{ergebnis.fehler ? (
<p role="alert" className="max-w-prose text-sm text-fehler">
{ergebnis.fehler}
</p>
) : null}
{ergebnis.erfolg ? (
<p role="status" className="text-sm text-erledigt">
{ergebnis.erfolg}
</p>
) : null}
{gesperrt ? null : (
<Button
type="submit"
disabled={laeuft}
className="h-11 self-start rounded-pille px-5 text-sm font-semibold"
>
{laeuft ? "Wird gespeichert …" : "Stammdaten speichern"}
</Button>
)}
</form>
);
}