Files
Florian-netz/src/components/admin/DataTable.tsx
Matthias Hochmeister e97e16d254 Workstream 6: Admin-Panel — Taxonomie & Bereitstellung (Phase 4)
Platform-Admin-only Oberflächen und Domänenlogik:

- codes.ts erweitert um allradCode/normalizeCode/codesMatch (Allrad-Infix
  kanonisch; Suche importiert weiterhin expandNameQuery). Pure-Unit-Tests.
- slug.ts (Idempotenz-Key-Erzeugung) + Tests.
- audit.ts: writeAudit mit EINER Signatur und optionalem typisierten tx.
- provisioning.ts: createBrigadeWithFirstAdmin (Geocoding inline, argon2id,
  Audit brigade.create/user.create) + resetUserPassword (Audit user.reset).
- Zod-Validierung: merkmal/template/equipment-category/brigade (+ Tests).
- Server Actions (jede mit Guard als erster Anweisung, default-deny):
  merkmale (CRUD, Delete blockiert bei Referenz), proposals (promote/merge mit
  Typ-Kompatibilität), templates (Merkmale/Vorgabewerte/Aliasse), equipment-
  categories, brigades (Bereitstellung/Reset). Audit in allen Schreib-Actions.
- (admin)-Route-Group: Layout mit requirePlatformAdmin als erster Zeile,
  AdminNav, DataTable, loading/error; Seiten für Merkmale (+Editor), Vorschläge
  (Merge), Vorlagen (+Detail mit Merkmal-/Alias-Editor und Allrad-Hinweis),
  Geräte-Kategorien (+Detail), Wehren (Liste/neu/Detail mit Passwort-Reset),
  paginierter Audit-Viewer mit Filter. Jede Seite ruft zusätzlich den Guard.
- i18n: admin-Strings in zentraler de.ts.
- Playwright-Specs (deferred, nicht ausgeführt): admin-gating,
  admin-merkmal-proposal, admin-brigade-provision.

Schema NICHT neu definiert — nur importiert. codes.ts ist hier Eigentümer.

Offline-Verifikation: tsc --noEmit grün; eslint grün; vitest run grün
(119 passed, 7 DB-roundtrip skipped); next build Exit 0; drizzle-kit check ok.
DB-/Server-/Browser-abhängige Schritte deferred (kein Postgres/Server im
Sandbox).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 10:30:52 +02:00

75 lines
1.9 KiB
TypeScript

import * as React from "react";
import { cn } from "@/lib/utils";
import { de } from "@/lib/i18n/de";
export interface Column<T> {
key: string;
header: string;
render: (row: T) => React.ReactNode;
className?: string;
}
/**
* Schlanke, server-renderbare Tabelle für Admin-Listen. Generisch über den
* Zeilentyp, keine Client-Interaktivität — Aktionen werden als `render`-Zellen
* (eigene Client-Komponenten) eingehängt. Empty-State (Querschnittsstandard 10).
*/
export function DataTable<T>({
columns,
rows,
getRowKey,
emptyText = de.admin.keineEintraege,
}: {
columns: Column<T>[];
rows: T[];
getRowKey: (row: T) => string;
emptyText?: string;
}) {
if (rows.length === 0) {
return (
<p className="rounded border border-rand bg-white px-4 py-6 text-sm text-anthrazit/60">
{emptyText}
</p>
);
}
return (
<div className="overflow-x-auto rounded border border-rand bg-white">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b border-rand text-left">
{columns.map((c) => (
<th
key={c.key}
scope="col"
className={cn(
"px-4 py-2 font-medium text-anthrazit/70",
c.className,
)}
>
{c.header}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr
key={getRowKey(row)}
className="border-b border-rand/60 last:border-0 hover:bg-nebel/60"
>
{columns.map((c) => (
<td
key={c.key}
className={cn("px-4 py-2 align-top", c.className)}
>
{c.render(row)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}