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>
This commit is contained in:
74
src/components/admin/DataTable.tsx
Normal file
74
src/components/admin/DataTable.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user