feat(cockpit): Baukasten aufgewertet — Slot-Wahl, Zahl-Spalte, Module bearbeiten, Unwetter-Vorlage (v1.8.2)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 44m40s

Journal modularer (Feedback Test-Feuerwehr Unwettereinsätze):
- Modul-Manager: Anzeige-Bereich (Hauptbereich/Seitenspalte) frei wählbar; bestehende
  Tabellen-Module bearbeitbar (Name/Slot/Spalten), nicht nur neu erstellen
- Neuer Spaltentyp "Zahl" (z.B. Auftragsnummer) inkl. Rendering/Eingabe im table-module
- Fertige Vorlage "Einsatz-Aufträge (Unwetter)": Zeit · Auftrags-Nr · Adresse · Auftrag ·
  Zugeteilt · erledigt (Hauptbereich, gross links)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-23 18:44:38 +02:00
parent f58f1b31bf
commit 9ba7194d41
4 changed files with 120 additions and 40 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "lageplan",
"version": "1.8.1",
"version": "1.8.2",
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
"private": true,
"scripts": {

View File

@@ -8,7 +8,7 @@ import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from '@/components/ui/dialog'
import {
ArrowUp, ArrowDown, Plus, Trash2, Loader2, Blocks, X,
ArrowUp, ArrowDown, Plus, Trash2, Loader2, Blocks, X, Pencil, PanelLeft, Columns2, Check,
} from 'lucide-react'
import { MODULE_PRESETS, type CockpitModule, type ModuleColumn, type ModuleColumnType } from '@/lib/modules'
@@ -30,10 +30,12 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState('')
// Neues Modul aus Vorlage
// Neues Modul aus Vorlage / Bearbeiten
const [showAdd, setShowAdd] = useState(false)
const [editId, setEditId] = useState<string | null>(null) // null = neu, sonst Modul-ID
const [presetKey, setPresetKey] = useState(MODULE_PRESETS[0].key)
const [newName, setNewName] = useState('')
const [newSlot, setNewSlot] = useState<'main' | 'side'>(MODULE_PRESETS[0].slot)
const [newColumns, setNewColumns] = useState<ModuleColumn[]>(MODULE_PRESETS[0].columns)
useEffect(() => {
@@ -41,6 +43,7 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
setWorking(modules)
setError('')
setShowAdd(false)
setEditId(null)
}
}, [open, modules])
@@ -64,25 +67,49 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
const preset = MODULE_PRESETS.find(p => p.key === key)!
setPresetKey(key)
setNewName(preset.key === 'leer' ? '' : preset.name)
setNewSlot(preset.slot)
setNewColumns(preset.columns)
}
const openNew = () => {
setEditId(null)
selectPreset(MODULE_PRESETS[0].key)
setShowAdd(true)
}
const openEdit = (m: CockpitModule) => {
setEditId(m.id)
setNewName(m.name)
setNewSlot(m.slot)
const cols: ModuleColumn[] = m.columns && m.columns.length ? m.columns : [{ key: 'text', label: 'Text', type: 'text' }]
setNewColumns(cols.map(c => ({ ...c })))
setShowAdd(true)
}
const addModule = () => {
const preset = MODULE_PRESETS.find(p => p.key === presetKey)!
const name = newName.trim() || preset.name
const mod: CockpitModule = {
id: `custom-${Date.now().toString(36)}`,
type: 'table',
name,
icon: preset.icon,
slot: preset.slot,
enabled: true,
sortOrder: working.length,
columns: newColumns.filter(c => c.label.trim()),
const columns = newColumns.filter(c => c.label.trim())
if (columns.length === 0) return
if (editId) {
// Bestehendes Modul aktualisieren (Name/Slot/Spalten)
setWorking(prev => prev.map(m => m.id === editId ? { ...m, name, slot: newSlot, columns } : m))
} else {
const mod: CockpitModule = {
id: `custom-${Date.now().toString(36)}`,
type: 'table',
name,
icon: preset.icon,
slot: newSlot,
enabled: true,
sortOrder: working.length,
columns,
}
setWorking(prev => [...prev, mod])
}
if (!mod.columns || mod.columns.length === 0) return
setWorking(prev => [...prev, mod])
setShowAdd(false)
setEditId(null)
setNewName('')
}
@@ -121,6 +148,7 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
const COLUMN_TYPES: { value: ModuleColumnType; label: string }[] = [
{ value: 'text', label: 'Text' },
{ value: 'number', label: 'Zahl' },
{ value: 'check', label: 'Haken' },
{ value: 'time', label: 'Zeit (auto)' },
]
@@ -167,6 +195,11 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
<button onClick={() => move(idx, 1)} disabled={idx === working.length - 1} className="p-1 text-muted-foreground hover:text-foreground disabled:opacity-30" title="Nach unten">
<ArrowDown className="w-3.5 h-3.5" />
</button>
{m.type === 'table' && (
<button onClick={() => openEdit(m)} className="p-1 text-muted-foreground hover:text-foreground" title="Modul bearbeiten">
<Pencil className="w-3.5 h-3.5" />
</button>
)}
{m.type === 'table' && (
<button onClick={() => removeCustom(idx)} className="p-1 text-muted-foreground hover:text-destructive" title="Modul löschen (Daten bleiben in der DB)">
<Trash2 className="w-3.5 h-3.5" />
@@ -177,40 +210,63 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
))}
</div>
{/* Neues Modul */}
{/* Neues Modul / Bearbeiten */}
{!showAdd ? (
<Button variant="outline" size="sm" onClick={() => { selectPreset(MODULE_PRESETS[0].key); setShowAdd(true) }} className="w-full">
<Button variant="outline" size="sm" onClick={openNew} className="w-full">
<Plus className="w-4 h-4 mr-1.5" /> Modul hinzufügen
</Button>
) : (
<div className="rounded-lg border p-3 space-y-3 bg-muted/20">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold">Neues Modul</p>
<button onClick={() => setShowAdd(false)} className="text-muted-foreground hover:text-foreground"><X className="w-4 h-4" /></button>
<p className="text-sm font-semibold">{editId ? 'Modul bearbeiten' : 'Neues Modul'}</p>
<button onClick={() => { setShowAdd(false); setEditId(null) }} className="text-muted-foreground hover:text-foreground"><X className="w-4 h-4" /></button>
</div>
{/* Vorlagen */}
<div className="grid grid-cols-2 gap-1.5">
{MODULE_PRESETS.map(p => (
<button
key={p.key}
onClick={() => selectPreset(p.key)}
className={`text-left rounded-md border-2 px-2 py-1.5 transition-colors ${
presetKey === p.key ? 'border-primary bg-primary/5' : 'border-border hover:border-muted-foreground'
}`}
title={p.description}
>
<p className="text-xs font-medium">{p.name}</p>
<p className="text-[10px] text-muted-foreground line-clamp-1">{p.description}</p>
</button>
))}
</div>
{/* Vorlagen — nur beim Neuanlegen */}
{!editId && (
<div className="grid grid-cols-2 gap-1.5">
{MODULE_PRESETS.map(p => (
<button
key={p.key}
onClick={() => selectPreset(p.key)}
className={`text-left rounded-md border-2 px-2 py-1.5 transition-colors ${
presetKey === p.key ? 'border-primary bg-primary/5' : 'border-border hover:border-muted-foreground'
}`}
title={p.description}
>
<p className="text-xs font-medium">{p.name}</p>
<p className="text-[10px] text-muted-foreground line-clamp-1">{p.description}</p>
</button>
))}
</div>
)}
<div className="space-y-1.5">
<Label className="text-xs">Name</Label>
<Input value={newName} onChange={e => setNewName(e.target.value)} placeholder="z.B. Atemschutz Zug 1" className="h-8 text-sm" />
</div>
{/* Anzeige-Bereich (Slot) frei wählbar */}
<div className="space-y-1.5">
<Label className="text-xs">Anzeige</Label>
<div className="grid grid-cols-2 gap-1.5">
<button
type="button"
onClick={() => setNewSlot('main')}
className={`flex items-center gap-1.5 rounded-md border-2 px-2 py-1.5 text-xs transition-colors ${newSlot === 'main' ? 'border-primary bg-primary/5' : 'border-border hover:border-muted-foreground'}`}
>
<PanelLeft className="w-3.5 h-3.5" /> Hauptbereich (gross)
</button>
<button
type="button"
onClick={() => setNewSlot('side')}
className={`flex items-center gap-1.5 rounded-md border-2 px-2 py-1.5 text-xs transition-colors ${newSlot === 'side' ? 'border-primary bg-primary/5' : 'border-border hover:border-muted-foreground'}`}
>
<Columns2 className="w-3.5 h-3.5" /> Seitenspalte
</button>
</div>
</div>
{/* Spalten-Editor */}
<div className="space-y-1.5">
<Label className="text-xs">Spalten</Label>
@@ -242,7 +298,7 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
</div>
<Button size="sm" onClick={addModule} disabled={newColumns.filter(c => c.label.trim()).length === 0} className="w-full">
<Plus className="w-4 h-4 mr-1" /> Modul erstellen
{editId ? <><Check className="w-4 h-4 mr-1" /> Änderungen übernehmen</> : <><Plus className="w-4 h-4 mr-1" /> Modul erstellen</>}
</Button>
</div>
)}

View File

@@ -35,7 +35,8 @@ export function TableModule({ projectId, module, canEdit, notifyChanged }: Table
const [draft, setDraft] = useState<Record<string, string>>({})
const columns: ModuleColumn[] = module.columns || []
const textColumns = columns.filter(c => c.type === 'text')
// Eingebbare Spalten: Text UND Zahl (Zeit = auto, Haken = Klick)
const textColumns = columns.filter(c => c.type === 'text' || c.type === 'number')
const load = useCallback(async () => {
try {
@@ -116,7 +117,7 @@ export function TableModule({ projectId, module, canEdit, notifyChanged }: Table
// Grid-Template aus Spalten ableiten (+ Lösch-Spalte)
const gridCols = columns.map(c =>
c.type === 'check' ? '32px' : c.type === 'time' ? '52px' : 'minmax(0,1fr)'
c.type === 'check' ? '32px' : c.type === 'time' ? '52px' : c.type === 'number' ? 'minmax(0,0.6fr)' : 'minmax(0,1fr)'
).join(' ') + (canEdit ? ' 24px' : '')
return (
@@ -167,6 +168,13 @@ export function TableModule({ projectId, module, canEdit, notifyChanged }: Table
</div>
)
}
if (c.type === 'number') {
return (
<div key={c.key} className="px-1.5 py-1.5 font-mono tabular-nums text-right min-w-0 truncate">
{item.data[c.key] || <span className="text-muted-foreground"></span>}
</div>
)
}
return (
<div key={c.key} className="px-1.5 py-1.5 break-words min-w-0">
{item.data[c.key] || <span className="text-muted-foreground"></span>}
@@ -193,10 +201,11 @@ export function TableModule({ projectId, module, canEdit, notifyChanged }: Table
<Input
key={c.key}
placeholder={c.label + '…'}
inputMode={c.type === 'number' ? 'numeric' : undefined}
value={draft[c.key] || ''}
onChange={(e) => setDraft(prev => ({ ...prev, [c.key]: e.target.value }))}
onKeyDown={(e) => e.key === 'Enter' && addRow()}
className={`h-6 text-[11px] ${i === 0 ? 'flex-1 min-w-0' : 'w-20 shrink-0'}`}
className={`h-6 text-[11px] ${i === 0 && c.type !== 'number' ? 'flex-1 min-w-0' : 'w-20 shrink-0'}`}
/>
))}
<Button size="sm" variant="ghost" onClick={addRow} disabled={!textColumns.some(c => (draft[c.key] || '').trim())} className="h-6 px-1.5 shrink-0">

View File

@@ -10,7 +10,7 @@
* Ohne Konfiguration gelten die DEFAULT_MODULES (heutiges Verhalten).
*/
export type ModuleColumnType = 'text' | 'check' | 'time'
export type ModuleColumnType = 'text' | 'number' | 'check' | 'time'
export interface ModuleColumn {
key: string
@@ -96,6 +96,21 @@ export const MODULE_PRESETS: { key: string; name: string; icon: string; slot: 'm
{ key: 'bedarf', label: 'Bedarf', type: 'text' },
],
},
{
key: 'unwetter',
name: 'Einsatz-Aufträge (Unwetter)',
icon: 'clipboard-list',
slot: 'main',
description: 'Auftragsliste gross links: Zeit · Auftrags-Nr · Adresse · Auftrag · Zugeteilt · erledigt',
columns: [
{ key: 'zeit', label: 'Zeit', type: 'time', width: 'w-16' },
{ key: 'auftragsnr', label: 'Auftrags-Nr', type: 'number', width: 'w-20' },
{ key: 'adresse', label: 'Adresse', type: 'text' },
{ key: 'auftrag', label: 'Auftrag', type: 'text' },
{ key: 'zugeteilt', label: 'Zugeteilt', type: 'text', width: 'w-24' },
{ key: 'erledigt', label: 'Ok', type: 'check', width: 'w-10' },
],
},
{
key: 'leer',
name: 'Eigenes Modul',
@@ -135,7 +150,7 @@ export function normalizeModules(raw: unknown): CockpitModule[] {
? (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',
type: (['text', 'number', 'check', 'time'] as const).includes(c.type) ? c.type : 'text',
width: typeof c.width === 'string' ? c.width : undefined,
}))
: undefined,