feat(stammdaten): Mitarbeiter, Abteilungen und Geraete (#25)
This commit was merged in pull request #25.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { Building, Plus } from "lucide-react";
|
||||
import { AbteilungArchivieren } from "@/components/abteilung-archivieren";
|
||||
import { AbteilungFormular } from "@/components/abteilung-formular";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { kundenEbeneSichtbar } from "@/server/mandant";
|
||||
import { kundenListe } from "@/server/queries/kunden";
|
||||
import { abteilungsListe, ungepflegteAbteilungen } from "@/server/queries/abteilungen";
|
||||
|
||||
/**
|
||||
* Abteilungen (#011).
|
||||
*
|
||||
* An einer Abteilung haengen Ziel-OU und Standardgruppen — daraus entstehen in
|
||||
* #012 die Befehle, die ein Konto anlegen. Deshalb verlangt diese Seite
|
||||
* `verwaltung` und nicht bloss Lesen: wer sie sieht, sieht die Struktur des
|
||||
* Verzeichnisses.
|
||||
*/
|
||||
|
||||
export default async function AbteilungenSeite() {
|
||||
// Jede Seite prueft selbst – das Admin-Layout darueber ist das zweite Netz,
|
||||
// nicht das erste (siehe CLAUDE.md).
|
||||
const { benutzer } = await sitzungErforderlich("verwaltung", "/admin/abteilungen");
|
||||
|
||||
const [zeilen, offene, kundenEbene] = await Promise.all([
|
||||
abteilungsListe(benutzer),
|
||||
ungepflegteAbteilungen(benutzer),
|
||||
kundenEbeneSichtbar(),
|
||||
]);
|
||||
const kunden = kundenEbene ? await kundenListe(benutzer) : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-2xl font-bold">Abteilungen</h1>
|
||||
<p className="mt-2 max-w-prose text-nebentext">
|
||||
Ziel-OU und Standardgruppen einer Abteilung bestimmen, wo ein neues Konto landet und in welchen
|
||||
Gruppen es liegt. Die Zuordnung läuft über den Abteilungsnamen der Person — ein Wert aus dem
|
||||
Verzeichnis bleibt deshalb auch dann erhalten, wenn es dazu noch keine gepflegte Abteilung gibt.
|
||||
</p>
|
||||
|
||||
{offene.length > 0 ? (
|
||||
<section className="mt-6 rounded-karte border border-linie bg-flaeche p-5">
|
||||
<h2 className="text-lg font-semibold text-faellig">Noch nicht gepflegt</h2>
|
||||
<p className="mt-1 max-w-prose text-sm text-nebentext">
|
||||
Diese Abteilungsnamen kommen bei Personen vor, es gibt aber keine Abteilung dazu. Ohne Ziel-OU und
|
||||
Standardgruppen bleiben die Platzhalter <code className="mono">{"{{ou}}"}</code> und{" "}
|
||||
<code className="mono">{"{{gruppen}}"}</code> im Lauf leer.
|
||||
</p>
|
||||
<ul className="mt-3 flex flex-wrap gap-2">
|
||||
{offene.map((eintrag) => (
|
||||
<li
|
||||
key={eintrag.name}
|
||||
className="rounded-pille bg-flaeche-hoch px-3 py-1.5 text-sm text-nebentext"
|
||||
>
|
||||
{eintrag.name}{" "}
|
||||
<span className="text-xs">
|
||||
({eintrag.anzahl} {eintrag.anzahl === 1 ? "Person" : "Personen"})
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<ul className="mt-6 flex flex-col gap-3">
|
||||
{zeilen.map((zeile) => (
|
||||
<li key={zeile.id} className="rounded-karte border border-linie bg-flaeche">
|
||||
<div className="flex flex-col gap-3 p-4 md:flex-row md:items-start md:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Building
|
||||
className="size-[18px] shrink-0 text-nebentext"
|
||||
strokeWidth={1.75}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className={zeile.archiviert ? "text-nebentext line-through" : "font-medium"}>
|
||||
{zeile.name}
|
||||
</span>
|
||||
<span className="text-xs text-nebentext">
|
||||
{zeile.anzahlMitarbeiter} {zeile.anzahlMitarbeiter === 1 ? "Person" : "Personen"}
|
||||
</span>
|
||||
</div>
|
||||
{zeile.zielOu ? <p className="mono mt-1 text-xs text-nebentext">{zeile.zielOu}</p> : null}
|
||||
{zeile.standardGruppen.length > 0 ? (
|
||||
<p className="mono mt-1 text-xs text-nebentext">{zeile.standardGruppen.join(", ")}</p>
|
||||
) : null}
|
||||
{zeile.mailDomain ? <p className="mt-1 text-xs text-nebentext">@{zeile.mailDomain}</p> : null}
|
||||
{zeile.notizen ? (
|
||||
<p className="mt-2 max-w-prose text-sm text-nebentext">{zeile.notizen}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<AbteilungArchivieren abteilungId={zeile.id} archiviert={zeile.archiviert} />
|
||||
</div>
|
||||
|
||||
<details className="group">
|
||||
<summary className="flex min-h-11 cursor-pointer list-none items-center px-4 text-sm text-nebentext hover:text-text">
|
||||
Bearbeiten
|
||||
</summary>
|
||||
<div className="px-4 pb-4">
|
||||
<AbteilungFormular
|
||||
werte={{
|
||||
id: zeile.id,
|
||||
name: zeile.name,
|
||||
zielOu: zeile.zielOu,
|
||||
standardGruppen: zeile.standardGruppen,
|
||||
mailDomain: zeile.mailDomain,
|
||||
notizen: zeile.notizen,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<section className="mt-8 rounded-karte border border-linie bg-flaeche p-5">
|
||||
<h2 className="flex items-center gap-2 text-lg font-semibold">
|
||||
<Plus className="size-5 text-akzent" strokeWidth={2} aria-hidden="true" />
|
||||
Abteilung anlegen
|
||||
</h2>
|
||||
<p className="mt-1 mb-5 max-w-prose text-sm text-nebentext">
|
||||
Der Name muss genauso lauten wie im Verzeichnis, sonst findet die Zuordnung ihn nicht.
|
||||
</p>
|
||||
<AbteilungFormular kunden={kunden.map(({ id, name }) => ({ id, name }))} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
const REITER = [
|
||||
{ pfad: "/admin", beschriftung: "Einstellungen" },
|
||||
{ pfad: "/admin/benutzer", beschriftung: "Benutzer" },
|
||||
{ pfad: "/admin/abteilungen", beschriftung: "Abteilungen" },
|
||||
] as const;
|
||||
|
||||
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
Reference in New Issue
Block a user