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>
153 lines
5.6 KiB
TypeScript
153 lines
5.6 KiB
TypeScript
/**
|
|
* Modul-Baukasten für das Einsatz-Cockpit (Journal-Bereich).
|
|
*
|
|
* Jede Feuerwehr stellt sich ihr Cockpit selbst zusammen:
|
|
* - Eingebaute Module: Journal, SOMA, Pendenzen
|
|
* - Generische Tabellen-Module ("table"): frei definierbare Spalten
|
|
* (Checklisten, Atemschutz-Überwachung, Kräfte vor Ort, Lagemeldungen …)
|
|
*
|
|
* Die Konfiguration liegt pro Mandant in `tenants.modulesConfig` (JSONB).
|
|
* Ohne Konfiguration gelten die DEFAULT_MODULES (heutiges Verhalten).
|
|
*/
|
|
|
|
export type ModuleColumnType = 'text' | 'check' | 'time'
|
|
|
|
export interface ModuleColumn {
|
|
key: string
|
|
label: string
|
|
type: ModuleColumnType
|
|
/** Optionale Tailwind-Breite für die Spalte, z.B. 'w-20' */
|
|
width?: string
|
|
}
|
|
|
|
export type ModuleType = 'journal' | 'soma' | 'pendenzen' | 'table'
|
|
|
|
export interface CockpitModule {
|
|
/** Eindeutig pro Mandant. Eingebaute Module: 'journal' | 'soma' | 'pendenzen' */
|
|
id: string
|
|
type: ModuleType
|
|
name: string
|
|
/** Lucide-Icon-Schlüssel (siehe MODULE_ICONS im Cockpit) */
|
|
icon: string
|
|
/** 'main' = breite Hauptspalte, 'side' = schmale Seitenspalte */
|
|
slot: 'main' | 'side'
|
|
enabled: boolean
|
|
sortOrder: number
|
|
/** Nur für type 'table': Spaltendefinition */
|
|
columns?: ModuleColumn[]
|
|
}
|
|
|
|
/** Standard-Cockpit — entspricht dem bisherigen festen Aufbau */
|
|
export const DEFAULT_MODULES: CockpitModule[] = [
|
|
{ id: 'journal', type: 'journal', name: 'Journal', icon: 'clipboard-list', slot: 'main', enabled: true, sortOrder: 0 },
|
|
{ id: 'soma', type: 'soma', name: 'SOMA', icon: 'alert-triangle', slot: 'side', enabled: true, sortOrder: 1 },
|
|
{ id: 'pendenzen', type: 'pendenzen', name: 'Pendenzen', icon: 'check-square', slot: 'side', enabled: true, sortOrder: 2 },
|
|
]
|
|
|
|
/** Vorlagen für neue Tabellen-Module — ein Klick, fertig konfiguriert */
|
|
export const MODULE_PRESETS: { key: string; name: string; icon: string; slot: 'main' | 'side'; description: string; columns: ModuleColumn[] }[] = [
|
|
{
|
|
key: 'checkliste',
|
|
name: 'Checkliste',
|
|
icon: 'list-checks',
|
|
slot: 'side',
|
|
description: 'Einfache Abhak-Liste (z.B. Rückzugskontrolle, Alarmstufen)',
|
|
columns: [
|
|
{ key: 'label', label: 'Punkt', type: 'text' },
|
|
{ key: 'done', label: 'Ok', type: 'check', width: 'w-10' },
|
|
],
|
|
},
|
|
{
|
|
key: 'atemschutz',
|
|
name: 'Atemschutz-Überwachung',
|
|
icon: 'shield',
|
|
slot: 'side',
|
|
description: 'Trupps mit Druck und Zeit überwachen',
|
|
columns: [
|
|
{ key: 'trupp', label: 'Trupp', type: 'text' },
|
|
{ key: 'druck', label: 'Druck (bar)', type: 'text', width: 'w-20' },
|
|
{ key: 'start', label: 'Eingesetzt', type: 'time', width: 'w-16' },
|
|
{ key: 'out', label: 'Draussen', type: 'check', width: 'w-10' },
|
|
],
|
|
},
|
|
{
|
|
key: 'kraefte',
|
|
name: 'Kräfte vor Ort',
|
|
icon: 'users',
|
|
slot: 'side',
|
|
description: 'Wer/was ist am Einsatzort (AdF, Fahrzeuge, Partner)',
|
|
columns: [
|
|
{ key: 'name', label: 'Name / Mittel', type: 'text' },
|
|
{ key: 'funktion', label: 'Funktion', type: 'text', width: 'w-24' },
|
|
{ key: 'seit', label: 'Vor Ort seit', type: 'time', width: 'w-16' },
|
|
{ key: 'abgemeldet', label: 'Weg', type: 'check', width: 'w-10' },
|
|
],
|
|
},
|
|
{
|
|
key: 'lagemeldung',
|
|
name: 'Lagemeldungen',
|
|
icon: 'radio',
|
|
slot: 'main',
|
|
description: 'Strukturierte Lagemeldungen (Zeit / Lage / Massnahmen / Bedarf)',
|
|
columns: [
|
|
{ key: 'zeit', label: 'Zeit', type: 'time', width: 'w-16' },
|
|
{ key: 'lage', label: 'Lage', type: 'text' },
|
|
{ key: 'massnahmen', label: 'Massnahmen', type: 'text' },
|
|
{ key: 'bedarf', label: 'Bedarf', type: 'text' },
|
|
],
|
|
},
|
|
{
|
|
key: 'leer',
|
|
name: 'Eigenes Modul',
|
|
icon: 'table',
|
|
slot: 'side',
|
|
description: 'Leere Tabelle — Spalten komplett selbst definieren',
|
|
columns: [
|
|
{ key: 'text', label: 'Text', type: 'text' },
|
|
],
|
|
},
|
|
]
|
|
|
|
/**
|
|
* Konfiguration validieren/reparieren: eingebaute Module sicherstellen,
|
|
* Unbekanntes verwerfen, stabil sortieren.
|
|
*/
|
|
export function normalizeModules(raw: unknown): CockpitModule[] {
|
|
if (!Array.isArray(raw) || raw.length === 0) return DEFAULT_MODULES
|
|
|
|
const seen = new Set<string>()
|
|
const result: CockpitModule[] = []
|
|
|
|
for (const m of raw as any[]) {
|
|
if (!m || typeof m.id !== 'string' || seen.has(m.id)) continue
|
|
const type: ModuleType = ['journal', 'soma', 'pendenzen', 'table'].includes(m.type) ? m.type : 'table'
|
|
if (type === 'table' && (!Array.isArray(m.columns) || m.columns.length === 0)) continue
|
|
seen.add(m.id)
|
|
result.push({
|
|
id: m.id,
|
|
type,
|
|
name: typeof m.name === 'string' && m.name.trim() ? m.name.trim().slice(0, 60) : 'Modul',
|
|
icon: typeof m.icon === 'string' ? m.icon : 'table',
|
|
slot: m.slot === 'main' ? 'main' : 'side',
|
|
enabled: m.enabled !== false,
|
|
sortOrder: typeof m.sortOrder === 'number' ? m.sortOrder : result.length,
|
|
columns: type === 'table'
|
|
? (m.columns as any[]).slice(0, 8).map((c, i) => ({
|
|
key: typeof c.key === 'string' && c.key ? c.key : `col${i}`,
|
|
label: typeof c.label === 'string' ? c.label.slice(0, 40) : `Spalte ${i + 1}`,
|
|
type: (['text', 'check', 'time'] as const).includes(c.type) ? c.type : 'text',
|
|
width: typeof c.width === 'string' ? c.width : undefined,
|
|
}))
|
|
: undefined,
|
|
})
|
|
}
|
|
|
|
// Eingebaute Module ergänzen, falls sie in der Config fehlen (dann deaktiviert lassen? → aktiv,
|
|
// damit ein kaputter Config-Stand nie das Journal verschwinden lässt)
|
|
for (const def of DEFAULT_MODULES) {
|
|
if (!seen.has(def.id)) result.push({ ...def, sortOrder: result.length })
|
|
}
|
|
|
|
return result.sort((a, b) => a.sortOrder - b.sortOrder)
|
|
}
|