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:
108
src/components/journal/modules/pendenzen-module.tsx
Normal file
108
src/components/journal/modules/pendenzen-module.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Plus, Trash2, Check } from 'lucide-react'
|
||||
import { type JournalPendenz, formatTime } from '@/components/journal/types'
|
||||
|
||||
interface PendenzenModuleProps {
|
||||
items: JournalPendenz[]
|
||||
canEdit: boolean
|
||||
onAdd: (what: string, who: string, whenHow: string) => void
|
||||
onToggle: (p: JournalPendenz) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
/** Pendenzen — eingebautes Cockpit-Modul (Was/Wer/Wann mit Erledigt-Haken) */
|
||||
export function PendenzenModule({ items, canEdit, onAdd, onToggle, onDelete }: PendenzenModuleProps) {
|
||||
const [what, setWhat] = useState('')
|
||||
const [who, setWho] = useState('')
|
||||
const [whenHow, setWhenHow] = useState('')
|
||||
|
||||
const submit = () => {
|
||||
if (!what.trim()) return
|
||||
onAdd(what.trim(), who.trim(), whenHow.trim())
|
||||
setWhat('')
|
||||
setWho('')
|
||||
setWhenHow('')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-4 text-xs">
|
||||
Keine Pendenzen
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border/50">
|
||||
{items.map((p) => (
|
||||
<div key={p.id} className={`flex items-start gap-1.5 px-2 py-1.5 text-xs print:text-[9px] group ${p.done ? 'bg-green-50 dark:bg-green-950/20 print:bg-green-50' : ''}`}>
|
||||
<button
|
||||
onClick={() => canEdit && onToggle(p)}
|
||||
className={`w-4 h-4 mt-0.5 rounded border-2 flex items-center justify-center shrink-0 transition-colors ${
|
||||
p.done
|
||||
? 'bg-green-500 border-green-500 text-white'
|
||||
: 'border-muted-foreground/30 hover:border-primary'
|
||||
}`}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
{p.done && <Check className="w-2.5 h-2.5" />}
|
||||
</button>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className={`${p.done ? 'text-muted-foreground' : ''}`}>
|
||||
{p.what}
|
||||
{p.done && p.doneAt && (
|
||||
<span className="ml-1 text-green-600 text-[10px] print:text-[8px] font-medium no-underline">
|
||||
(erledigt um {formatTime(p.doneAt)})
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<div className="flex gap-2 text-[10px] text-muted-foreground">
|
||||
{p.who && <span>Wer: {p.who}</span>}
|
||||
{p.whenHow && <span>Wann: {p.whenHow}</span>}
|
||||
</div>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<button
|
||||
onClick={() => onDelete(p.id)}
|
||||
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-destructive shrink-0 mt-0.5 print:hidden"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{canEdit && (
|
||||
<div className="border-t border-border p-1.5 print:hidden">
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
placeholder="Was..."
|
||||
value={what}
|
||||
onChange={(e) => setWhat(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && submit()}
|
||||
className="flex-1 h-6 text-[11px]"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Wer"
|
||||
value={who}
|
||||
onChange={(e) => setWho(e.target.value)}
|
||||
className="w-14 h-6 text-[11px]"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Wann"
|
||||
value={whenHow}
|
||||
onChange={(e) => setWhenHow(e.target.value)}
|
||||
className="w-14 h-6 text-[11px]"
|
||||
/>
|
||||
<Button size="sm" variant="ghost" onClick={submit} disabled={!what.trim()} className="h-6 px-1.5">
|
||||
<Plus className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user