import { redirect } from "next/navigation"; import { AlertTriangle, CheckCircle2, Clock, Server } from "lucide-react"; import { Betriebsmodus } from "@/generated/prisma/enums"; import { JetztAbgleichen } from "@/components/verzeichnis/jetzt-abgleichen"; import { QuelleFormular } from "@/components/verzeichnis/quelle-formular"; import { VerbindungTesten } from "@/components/verzeichnis/verbindung-testen"; import { sitzungErforderlich } from "@/server/auth/sitzungspflicht"; import { betriebsmodus } from "@/server/mandant"; import { abgleichLog, type AbgleichLogZeile, quellenListe } from "@/server/queries/verzeichnis"; import type { QuellenSicht } from "@/server/services/verzeichnis"; /** * Admin → Verzeichnis (#016). * * Nur im Modus INTERN: dort hat das Haus **ein** Active Directory, aus dem seine * Leute kommen. Im Modus DIENSTLEISTUNG gäbe es Verzeichnisse fremder Kunden, * und die bindet diese Anwendung ausdrücklich nicht an (#008) — die Seite leitet * dann um, nicht nur der Reiter verschwindet. * * Die Reihenfolge auf der Seite ist die Reihenfolge der Arbeit: erst sehen, was * da ist und wie der letzte Abgleich lief, dann prüfen, dann anlegen. */ const ZEITPUNKT = new Intl.DateTimeFormat("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", }); function Status({ quelle }: { quelle: QuellenSicht }) { if (quelle.letzterAbgleichAm === null) { return (

Noch nie abgeglichen. „Verbindung testen“ zeigt, was ankäme, ohne etwas zu schreiben.

); } const gescheitert = quelle.letzterAbgleichStatus === "FEHLER"; return (

{gescheitert ? (

{quelle.letzterAbgleichFehler ? (

{quelle.letzterAbgleichFehler}

) : null}
); } /** * Das Abgleich-Log: was die letzten Läufe getan haben. * * Keine Tabelle, sondern eine Liste mit umbrechenden Zeilen — bei 390 px hätte * eine Tabelle mit sechs Zahlenspalten quer geschoben werden müssen. * * Gezeigt werden auch Nullen: „0 angelegt, 0 aktualisiert“ ist die Auskunft, dass * der Abgleich lief und das Verzeichnis sich nicht geändert hat. Fehlte die Zeile, * sähe ein erfolgreicher Lauf ohne Änderung aus wie ein Lauf, der nie stattfand. */ function Log({ zeilen }: { zeilen: readonly AbgleichLogZeile[] }) { if (zeilen.length === 0) { return (

Noch kein Lauf. Hier steht danach, was jeder Abgleich getan hat.

); } return ( ); } export default async function VerzeichnisSeite() { const { benutzer } = await sitzungErforderlich("verwaltung", "/admin/verzeichnis"); if ((await betriebsmodus()) !== Betriebsmodus.INTERN) { redirect("/admin"); } // Der angemeldete Benutzer, nicht ein erfundener: die Mandantensicht haengt // daran, und ein hart gesetzter „ADMIN“ waere eine Hintertuer. const handelnder = { id: benutzer.id, rolle: benutzer.rolle }; const quellen = await quellenListe(handelnder); // Je Quelle ein Log. Die Abfragen laufen parallel; es sind so viele wie Quellen, // und das sind im Modus INTERN eine oder zwei. const logs = new Map( await Promise.all( quellen.map(async (quelle) => [quelle.id, await abgleichLog(quelle.id, handelnder)] as const), ), ); return ( <>

Verzeichnis

Die Personen der eigenen Firma kommen aus dem Active Directory, damit beim Start eines Laufs nichts abgetippt werden muss. Wer dort verschwindet oder deaktiviert wird, wird hier inaktiv{" "} — gelöscht wird nie, sonst verlöre die Historie ihre Namen.

{quellen.length === 0 ? (

Es ist keine Quelle eingerichtet. Personen lassen sich auch von Hand anlegen oder aus einer CSV-Datei einlesen — ein Verzeichnis nimmt nur die Arbeit ab.

) : ( )}

Quelle anlegen

Ein Dienstkonto mit Leserechten genügt. Laufzettel schreibt nie ins Verzeichnis und speichert nur die zugeordneten Felder — keine Passwörter, keine Gruppenmitgliedschaften.

); }