Bisher liess sich nur je Schluessel oder je Host nachsehen, wo ein Key verwaltet ist. Die Matrix zeigt das auf einen Blick, loest Hostgruppen-Zuweisungen je Mitglied auf (P vs. P·G) und macht nicht synchrone Zustaende ohne Farbe erkennbar. Die Matrix plant nur: Zuweisen und Entfernen laufen ueber die vorhandenen Aktionen, ausgerollt wird weiter per Dry-Run auf der Schluesselseite. Der Aufbau ist eine reine Funktion in src/lib, damit Seite und CSV-Export garantiert dasselbe zeigen und die Gruppenaufloesung ohne Datenbank testbar ist. Der CSV-Export entschaerft Formelbeginne, weil Schluesselnamen frei waehlbar sind und Excel sie sonst ausfuehren wuerde. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
6.6 KiB
TypeScript
165 lines
6.6 KiB
TypeScript
import { Download, Network, Search } from "lucide-react";
|
||
import type { Metadata } from "next";
|
||
import { Zuweisungsmatrix } from "@/components/zuweisungen/Zuweisungsmatrix";
|
||
import { Button, buttonKlassen } from "@/components/ui/Button";
|
||
import { Ankreuzfeld, Auswahlliste, Feld } from "@/components/ui/Feld";
|
||
import { Kennzahl } from "@/components/ui/Kennzahl";
|
||
import { Leerzustand } from "@/components/ui/Leerzustand";
|
||
import { Seitenkopf } from "@/components/ui/Seitenkopf";
|
||
import { KATEGORIE_TEXT } from "@/lib/schluessel";
|
||
import { matrixFilterAus, matrixFilterQuery } from "@/lib/zuweisungsmatrix";
|
||
import { darf } from "@/server/auth/rechte";
|
||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||
import { zielAuswahl } from "@/server/queries/ziele";
|
||
import { zuweisungsMatrix } from "@/server/queries/zuweisungen";
|
||
|
||
export const metadata: Metadata = { title: "Zuweisungen" };
|
||
|
||
type Suchparameter = Promise<Record<string, string | string[] | undefined>>;
|
||
|
||
/**
|
||
* Zuweisungsmatrix Schluessel × Host (#025). Filter per GET, damit Links und der CSV-Export
|
||
* denselben Ausschnitt zeigen. Die Matrix plant nur – ausgerollt wird ueber den Dry-Run.
|
||
*/
|
||
export default async function Zuweisungen({ searchParams }: { searchParams: Suchparameter }) {
|
||
// Jede Seite prueft selbst; die Huelle ist nur das zweite Netz (CLAUDE.md).
|
||
const { benutzer } = await sitzungErforderlich("lesen", "/zuweisungen");
|
||
const filter = matrixFilterAus(await searchParams);
|
||
const darfZuweisen = darf(benutzer.rolle, "publicKeyAusrollen");
|
||
const [{ matrix, gruppen, personen }, auswahl] = await Promise.all([
|
||
zuweisungsMatrix(filter),
|
||
darfZuweisen ? zielAuswahl() : Promise.resolve(undefined),
|
||
]);
|
||
const gefiltert = Boolean(
|
||
filter.gruppeId || filter.kategorie || filter.personId || filter.q || filter.nurBelegt,
|
||
);
|
||
const { zaehler } = matrix;
|
||
const leer = matrix.zeilen.length === 0 || matrix.spalten.length === 0;
|
||
|
||
return (
|
||
<div className="flex min-w-0 flex-col gap-6 sm:gap-7">
|
||
<Seitenkopf
|
||
titel="Zuweisungen"
|
||
untertitel="Welcher Schlüssel auf welchem Host verwaltet ist"
|
||
aktionen={
|
||
<a
|
||
href={`/api/zuweisungen.csv${matrixFilterQuery(filter)}`}
|
||
download
|
||
className={buttonKlassen("sekundaer")}
|
||
>
|
||
<Download className="size-4" aria-hidden />
|
||
CSV exportieren
|
||
</a>
|
||
}
|
||
/>
|
||
|
||
<div className="grid grid-cols-2 gap-3 sm:gap-5 xl:grid-cols-4">
|
||
<Kennzahl titel="Schlüssel" wert={zaehler.schluessel} hinweis="aktiv, in der Ansicht" />
|
||
<Kennzahl titel="Hosts" wert={zaehler.hosts} hinweis="nicht archiviert" />
|
||
<Kennzahl titel="Zuweisungen" wert={zaehler.zuweisungen} hinweis="Gruppen je Host gezählt" />
|
||
<Kennzahl
|
||
titel="Nicht synchron"
|
||
wert={zaehler.nichtSynchron}
|
||
variante={zaehler.nichtSynchron > 0 ? "messing" : "neutral"}
|
||
hinweis="geplant, Drift, entfernen, Fehler"
|
||
/>
|
||
</div>
|
||
|
||
<form
|
||
method="get"
|
||
role="search"
|
||
className="grid items-end gap-3 sm:grid-cols-2 lg:grid-cols-[minmax(0,1fr)_11rem_10rem_11rem]"
|
||
>
|
||
<Feld
|
||
beschriftung="Suche"
|
||
name="q"
|
||
type="search"
|
||
defaultValue={filter.q}
|
||
placeholder="Schlüssel- oder Hostname"
|
||
autoComplete="off"
|
||
spellCheck={false}
|
||
className="sm:col-span-2 lg:col-span-1"
|
||
/>
|
||
<Auswahlliste beschriftung="Hostgruppe" name="gruppe" defaultValue={filter.gruppeId}>
|
||
<option value="">Alle Hosts</option>
|
||
{gruppen.map((g) => (
|
||
<option key={g.id} value={g.id}>
|
||
{g.name}
|
||
</option>
|
||
))}
|
||
</Auswahlliste>
|
||
<Auswahlliste beschriftung="Kategorie" name="kategorie" defaultValue={filter.kategorie}>
|
||
<option value="">Alle</option>
|
||
{Object.entries(KATEGORIE_TEXT).map(([wert, text]) => (
|
||
<option key={wert} value={wert}>
|
||
{text}
|
||
</option>
|
||
))}
|
||
</Auswahlliste>
|
||
<Auswahlliste beschriftung="Person" name="person" defaultValue={filter.personId}>
|
||
<option value="">Alle Personen</option>
|
||
{personen.map((p) => (
|
||
<option key={p.id} value={p.id}>
|
||
{p.name}
|
||
</option>
|
||
))}
|
||
</Auswahlliste>
|
||
<div className="flex flex-col gap-3 sm:col-span-2 sm:flex-row sm:items-center sm:justify-between lg:col-span-4">
|
||
<Ankreuzfeld
|
||
name="belegt"
|
||
value="1"
|
||
defaultChecked={filter.nurBelegt}
|
||
beschriftung="Nur Zeilen und Spalten mit Zuweisungen"
|
||
className="-mx-2"
|
||
/>
|
||
<div className="flex gap-2.5">
|
||
<Button type="submit" icon={<Search className="size-4" />} className="flex-1 sm:flex-none">
|
||
Filtern
|
||
</Button>
|
||
{gefiltert && (
|
||
<Button href="/zuweisungen" className="flex-1 sm:flex-none">
|
||
Zurücksetzen
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
{leer ? (
|
||
gefiltert ? (
|
||
<Leerzustand
|
||
icon={<Search className="size-5" />}
|
||
titel="Keine Treffer"
|
||
text="Kein Schlüssel und kein Host passen zu Suche und Filter."
|
||
aktion={<Button href="/zuweisungen">Filter zurücksetzen</Button>}
|
||
/>
|
||
) : (
|
||
<Leerzustand
|
||
icon={<Network className="size-5" />}
|
||
titel="Noch nichts zuzuordnen"
|
||
text="Die Matrix braucht mindestens einen aktiven Schlüssel und einen Host."
|
||
/>
|
||
)
|
||
) : (
|
||
<section className="flex min-w-0 flex-col gap-3" aria-label="Matrix">
|
||
<p className="text-muted text-[13px]">
|
||
<span className="text-text font-medium">P</span> Public Key direkt ·{" "}
|
||
<span className="text-text font-medium">P·G</span> über Hostgruppe ·{" "}
|
||
<span className="text-text font-medium">K</span> Private Key · <span className="text-ok">●</span>{" "}
|
||
synchron · <span className="text-accent">○</span> geplant oder wird entfernt ·{" "}
|
||
<span className="text-danger">●</span> Drift oder Fehler
|
||
</p>
|
||
<Zuweisungsmatrix
|
||
zeilen={matrix.zeilen}
|
||
spalten={matrix.spalten}
|
||
zellen={Object.fromEntries(matrix.zellen)}
|
||
darfZuweisen={darfZuweisen}
|
||
darfPrivatEntfernen={darf(benutzer.rolle, "privateKeyAusrollen")}
|
||
{...(auswahl ? { auswahl } : {})}
|
||
/>
|
||
</section>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|