feat(cockpit): Modul-Baukasten – Einsatz-Cockpit frei zusammenstellbar (v1.5.0)

Jede Feuerwehr stellt ihr Cockpit selbst zusammen (Wunsch: modular & einfach):

M0 – Zerlegt:
- SOMA + Pendenzen als eigenständige Modul-Komponenten, gemeinsame Typen
  (journal/types.ts), einheitliche ModuleCard-Hülle

M1 – Registry & Verwaltung:
- Modul-Registry src/lib/modules.ts, Konfiguration pro Mandant in
  tenants.modulesConfig (JSONB, idempotente Migration)
- API GET/PUT /api/tenant/modules (normalisiert + validiert via normalizeModules)
- "Module"-Button im Journal (Admins): an/aus, Reihenfolge hoch/runter,
  eigene Module löschen

M2 – Eigene Module:
- Generisches Tabellen-Modul: frei definierbare Spalten (Text / Haken mit
  Zeitstempel / Auto-Zeit), max 8 Spalten
- Generische Daten-API /api/projects/[id]/modules/[moduleId]/items (+ [itemId]),
  neue Tabelle module_items, Live-Sync über journal-refresh Events

M3 – Feld-Vorlagen:
- Checkliste, Atemschutz-Überwachung (Trupp/Druck/Zeit/Draussen),
  Kräfte vor Ort (Name/Funktion/seit/Weg), Lagemeldungen (Hauptbereich),
  leeres Modul mit Spalten-Editor

Eingebaute Module (Journal/SOMA/Pendenzen) bleiben ohne Konfiguration exakt
wie bisher — kaputte Configs fallen sicher auf den Standard zurück.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-19 18:15:34 +02:00
parent 6eab656ae6
commit 2dbb2204f6
14 changed files with 1412 additions and 412 deletions

View File

@@ -82,6 +82,8 @@ async function migrate() {
// Journal
`ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS "isCorrected" BOOLEAN DEFAULT false`,
`ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS "correctionOfId" TEXT`,
// Modul-Baukasten (Cockpit-Konfiguration pro Mandant)
`ALTER TABLE tenants ADD COLUMN IF NOT EXISTS "modulesConfig" JSONB`,
]
let added = 0
for (const sql of columnMigrations) {
@@ -131,6 +133,17 @@ async function migrate() {
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"projectId" TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE
)`,
// Modul-Baukasten: generische Zeilen für Tabellen-Module
`CREATE TABLE IF NOT EXISTS module_items (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
"moduleId" TEXT NOT NULL,
data JSONB NOT NULL DEFAULT '{}',
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"projectId" TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS module_items_project_module_idx ON module_items("projectId", "moduleId")`,
]
for (const sql of tableMigrations) {
try { await prisma.$executeRawUnsafe(sql) } catch (e) { /* table might already exist */ }

View File

@@ -74,6 +74,8 @@ model Tenant {
notes String?
hiddenIconIds String[] @default([])
journalSuggestions String[] @default([])
// Modul-Baukasten: Cockpit-Konfiguration (siehe src/lib/modules.ts). NULL = Standard.
modulesConfig Json?
// Privacy consent
privacyAccepted Boolean @default(false)
@@ -193,6 +195,7 @@ model Project {
journalPendenzen JournalPendenz[]
exerciseGoals ExerciseGoal[]
rapports Rapport[]
moduleItems ModuleItem[]
@@map("projects")
}
@@ -349,6 +352,24 @@ model JournalPendenz {
@@map("journal_pendenzen")
}
// Generischer Datenspeicher für Tabellen-Module (Modul-Baukasten).
// data enthält die Zellwerte gemäss Spaltendefinition des Moduls,
// z.B. { "trupp": "Trupp 1", "druck": "280", "out": true, "outAt": "..." }
model ModuleItem {
id String @id @default(uuid())
moduleId String
data Json @default("{}")
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@index([projectId, moduleId])
@@map("module_items")
}
model JournalCheckTemplate {
id String @id @default(uuid())
label String