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:
Matthias Hochmeister
2026-06-09 10:30:52 +02:00
parent 0a7173ef38
commit e97e16d254
49 changed files with 3676 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
import { describe, it, expect } from "vitest";
import { merkmalCreateSchema, merkmalUpdateSchema } from "./merkmal";
describe("merkmalCreateSchema", () => {
const base = {
name: "Löschwassertank",
typ: "number" as const,
geltungsbereich: "vehicle" as const,
einheit: "l",
};
it("akzeptiert ein number-Merkmal ohne Optionen", () => {
const r = merkmalCreateSchema.safeParse(base);
expect(r.success).toBe(true);
});
it("verlangt mindestens eine Option bei typ=enum", () => {
const r = merkmalCreateSchema.safeParse({
name: "Pumpentyp",
typ: "enum",
geltungsbereich: "vehicle",
optionen: [],
});
expect(r.success).toBe(false);
});
it("akzeptiert enum mit Optionen", () => {
const r = merkmalCreateSchema.safeParse({
name: "Pumpentyp",
typ: "enum",
geltungsbereich: "vehicle",
optionen: [{ wert: "fpn_10_1000", label: "FPN 10-1000" }],
});
expect(r.success).toBe(true);
});
it("lehnt leeren Namen ab", () => {
const r = merkmalCreateSchema.safeParse({ ...base, name: "" });
expect(r.success).toBe(false);
});
it("lehnt ungültigen Typ ab", () => {
const r = merkmalCreateSchema.safeParse({ ...base, typ: "datum" });
expect(r.success).toBe(false);
});
});
describe("merkmalUpdateSchema", () => {
it("verlangt eine id", () => {
const r = merkmalUpdateSchema.safeParse({
name: "X",
typ: "text",
geltungsbereich: "both",
});
expect(r.success).toBe(false);
});
it("akzeptiert vollständige Update-Daten", () => {
const r = merkmalUpdateSchema.safeParse({
id: "11111111-1111-1111-1111-111111111111",
name: "X",
typ: "text",
geltungsbereich: "both",
});
expect(r.success).toBe(true);
});
});