Files
Laufzettel/src/components/einstellungen-formular.tsx
T
florian.wildeandClaude Opus 5 ecc3c579df
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(mandant): Betriebsmodus, Kundenverwaltung und Einstellungen (#22)
Zwei Betriebsmodi, ein Codepfad. src/server/mandant.ts ist die eine Stelle,
an der die Mandantenregel steht.

Der Review fand drei Blocker: die Lesemodelle wandten die Mandantensicht nicht
an, der Protokolleintrag des Moduswechsels stand außerhalb der Transaktion, und
Prüfung und Vollzug lagen so weit auseinander, dass zwei gleichzeitige Wechsel
beide durchliefen. Alle drei geschlossen, der letzte mit einem Advisory Lock.

336 Tests.

Closes #8

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 05:03:11 +00:00

131 lines
4.7 KiB
TypeScript

"use client";
import { useActionState } from "react";
import { Button } from "@/components/ui/button";
import { einstellungenSpeichernAktion, type AktionsErgebnis } from "@/server/actions/einstellungen";
const FELD =
"min-h-11 rounded-karte border border-linie bg-grund px-3 text-sm text-text outline-none focus-visible:border-akzent";
export type EinstellungsWerte = {
readonly eigeneOrganisationName: string;
readonly gesperrteOu: string;
readonly regeln: {
readonly upn: string;
readonly sam: string;
readonly mail: string;
readonly hostname: string;
};
/** Im Modus Dienstleistung heisst die Organisation in der Oberflaeche "Kunde". */
readonly kundenEbeneSichtbar: boolean;
};
const REGELN = [
{
name: "regel_upn",
beschriftung: "UPN",
schluessel: "upn",
beispiel: "{vorname}.{nachname}@firma.de",
},
{ name: "regel_sam", beschriftung: "Anmeldename (sam)", schluessel: "sam", beispiel: "{v}{nachname}" },
{ name: "regel_mail", beschriftung: "Mail", schluessel: "mail", beispiel: "{vorname}.{nachname}@firma.de" },
{ name: "regel_hostname", beschriftung: "Hostname", schluessel: "hostname", beispiel: "PC-{nnn}" },
] as const;
export function EinstellungsFormular({ werte }: { werte: EinstellungsWerte }) {
const [ergebnis, aktion, laeuft] = useActionState<AktionsErgebnis, FormData>(
einstellungenSpeichernAktion,
{},
);
return (
<form action={aktion} className="flex flex-col gap-8">
<section>
<h2 className="text-lg font-semibold">
{werte.kundenEbeneSichtbar ? "Eigene Organisation" : "Organisation"}
</h2>
<p className="mt-1 mb-4 max-w-prose text-sm text-nebentext">
{werte.kundenEbeneSichtbar
? "Der eigene Betrieb, der im Modus Dienstleistung als erster Kunde geführt wird."
: "Im Modus Intern erscheint dieser Name nirgends in der Oberfläche. Er wird erst sichtbar, wenn du auf Dienstleistung umstellst."}
</p>
<label className="flex max-w-md flex-col gap-1.5 text-sm">
<span className="text-nebentext">Name</span>
<input
name="eigeneOrganisationName"
defaultValue={werte.eigeneOrganisationName}
required
className={FELD}
/>
</label>
</section>
<section>
<h2 className="text-lg font-semibold">Namensregeln</h2>
<p className="mt-1 mb-4 max-w-prose text-sm text-nebentext">
Vorlagen für UPN, Anmeldename, Mail und Hostname. Beim Start eines Laufs werden daraus Vorschläge
berechnet, die der Techniker überschreiben kann. Platzhalter:{" "}
<code className="mono">{"{vorname}"}</code>, <code className="mono">{"{nachname}"}</code>,{" "}
<code className="mono">{"{v}"}</code> für den ersten Buchstaben des Vornamens,{" "}
<code className="mono">{"{nnn}"}</code> für eine fortlaufende Nummer
{werte.kundenEbeneSichtbar ? (
<>
{" "}
und <code className="mono">{"{kundenkuerzel}"}</code>
</>
) : null}
.
</p>
<div className="grid gap-4 sm:grid-cols-2">
{REGELN.map((regel) => (
<label key={regel.name} className="flex flex-col gap-1.5 text-sm">
<span className="text-nebentext">{regel.beschriftung}</span>
<input
name={regel.name}
defaultValue={werte.regeln[regel.schluessel]}
placeholder={regel.beispiel}
className={`${FELD} mono`}
/>
</label>
))}
</div>
</section>
<section>
<h2 className="text-lg font-semibold">Sperr-OU</h2>
<p className="mt-1 mb-4 max-w-prose text-sm text-nebentext">
Ziel-OU für deaktivierte Konten. Wird beim Offboarding vorbelegt.
</p>
<label className="flex max-w-lg flex-col gap-1.5 text-sm">
<span className="text-nebentext">Organisationseinheit</span>
<input
name="gesperrteOu"
defaultValue={werte.gesperrteOu}
placeholder="OU=Gesperrt,OU=Benutzer,DC=firma,DC=local"
className={`${FELD} mono`}
/>
</label>
</section>
{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}
<Button
type="submit"
disabled={laeuft}
className="h-11 self-start rounded-pille px-5 text-sm font-semibold"
>
{laeuft ? "Wird gespeichert …" : "Speichern"}
</Button>
</form>
);
}