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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e3d80e5982
commit
86d55917f9
@@ -0,0 +1,153 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user