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>
154 lines
5.5 KiB
TypeScript
154 lines
5.5 KiB
TypeScript
"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>
|
||
);
|
||
}
|