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
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:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lageplan",
|
"name": "lageplan",
|
||||||
"version": "1.8.1",
|
"version": "1.8.2",
|
||||||
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import {
|
import {
|
||||||
ArrowUp, ArrowDown, Plus, Trash2, Loader2, Blocks, X,
|
ArrowUp, ArrowDown, Plus, Trash2, Loader2, Blocks, X, Pencil, PanelLeft, Columns2, Check,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { MODULE_PRESETS, type CockpitModule, type ModuleColumn, type ModuleColumnType } from '@/lib/modules'
|
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 [isSaving, setIsSaving] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
// Neues Modul aus Vorlage
|
// Neues Modul aus Vorlage / Bearbeiten
|
||||||
const [showAdd, setShowAdd] = useState(false)
|
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 [presetKey, setPresetKey] = useState(MODULE_PRESETS[0].key)
|
||||||
const [newName, setNewName] = useState('')
|
const [newName, setNewName] = useState('')
|
||||||
|
const [newSlot, setNewSlot] = useState<'main' | 'side'>(MODULE_PRESETS[0].slot)
|
||||||
const [newColumns, setNewColumns] = useState<ModuleColumn[]>(MODULE_PRESETS[0].columns)
|
const [newColumns, setNewColumns] = useState<ModuleColumn[]>(MODULE_PRESETS[0].columns)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,6 +43,7 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
|
|||||||
setWorking(modules)
|
setWorking(modules)
|
||||||
setError('')
|
setError('')
|
||||||
setShowAdd(false)
|
setShowAdd(false)
|
||||||
|
setEditId(null)
|
||||||
}
|
}
|
||||||
}, [open, modules])
|
}, [open, modules])
|
||||||
|
|
||||||
@@ -64,25 +67,49 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
|
|||||||
const preset = MODULE_PRESETS.find(p => p.key === key)!
|
const preset = MODULE_PRESETS.find(p => p.key === key)!
|
||||||
setPresetKey(key)
|
setPresetKey(key)
|
||||||
setNewName(preset.key === 'leer' ? '' : preset.name)
|
setNewName(preset.key === 'leer' ? '' : preset.name)
|
||||||
|
setNewSlot(preset.slot)
|
||||||
setNewColumns(preset.columns)
|
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 addModule = () => {
|
||||||
const preset = MODULE_PRESETS.find(p => p.key === presetKey)!
|
const preset = MODULE_PRESETS.find(p => p.key === presetKey)!
|
||||||
const name = newName.trim() || preset.name
|
const name = newName.trim() || preset.name
|
||||||
|
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 = {
|
const mod: CockpitModule = {
|
||||||
id: `custom-${Date.now().toString(36)}`,
|
id: `custom-${Date.now().toString(36)}`,
|
||||||
type: 'table',
|
type: 'table',
|
||||||
name,
|
name,
|
||||||
icon: preset.icon,
|
icon: preset.icon,
|
||||||
slot: preset.slot,
|
slot: newSlot,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
sortOrder: working.length,
|
sortOrder: working.length,
|
||||||
columns: newColumns.filter(c => c.label.trim()),
|
columns,
|
||||||
}
|
}
|
||||||
if (!mod.columns || mod.columns.length === 0) return
|
|
||||||
setWorking(prev => [...prev, mod])
|
setWorking(prev => [...prev, mod])
|
||||||
|
}
|
||||||
setShowAdd(false)
|
setShowAdd(false)
|
||||||
|
setEditId(null)
|
||||||
setNewName('')
|
setNewName('')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +148,7 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
|
|||||||
|
|
||||||
const COLUMN_TYPES: { value: ModuleColumnType; label: string }[] = [
|
const COLUMN_TYPES: { value: ModuleColumnType; label: string }[] = [
|
||||||
{ value: 'text', label: 'Text' },
|
{ value: 'text', label: 'Text' },
|
||||||
|
{ value: 'number', label: 'Zahl' },
|
||||||
{ value: 'check', label: 'Haken' },
|
{ value: 'check', label: 'Haken' },
|
||||||
{ value: 'time', label: 'Zeit (auto)' },
|
{ 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">
|
<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" />
|
<ArrowDown className="w-3.5 h-3.5" />
|
||||||
</button>
|
</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' && (
|
{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)">
|
<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" />
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
@@ -177,19 +210,20 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Neues Modul */}
|
{/* Neues Modul / Bearbeiten */}
|
||||||
{!showAdd ? (
|
{!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
|
<Plus className="w-4 h-4 mr-1.5" /> Modul hinzufügen
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border p-3 space-y-3 bg-muted/20">
|
<div className="rounded-lg border p-3 space-y-3 bg-muted/20">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm font-semibold">Neues Modul</p>
|
<p className="text-sm font-semibold">{editId ? 'Modul bearbeiten' : 'Neues Modul'}</p>
|
||||||
<button onClick={() => setShowAdd(false)} className="text-muted-foreground hover:text-foreground"><X className="w-4 h-4" /></button>
|
<button onClick={() => { setShowAdd(false); setEditId(null) }} className="text-muted-foreground hover:text-foreground"><X className="w-4 h-4" /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vorlagen */}
|
{/* Vorlagen — nur beim Neuanlegen */}
|
||||||
|
{!editId && (
|
||||||
<div className="grid grid-cols-2 gap-1.5">
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
{MODULE_PRESETS.map(p => (
|
{MODULE_PRESETS.map(p => (
|
||||||
<button
|
<button
|
||||||
@@ -205,12 +239,34 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs">Name</Label>
|
<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" />
|
<Input value={newName} onChange={e => setNewName(e.target.value)} placeholder="z.B. Atemschutz Zug 1" className="h-8 text-sm" />
|
||||||
</div>
|
</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 */}
|
{/* Spalten-Editor */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs">Spalten</Label>
|
<Label className="text-xs">Spalten</Label>
|
||||||
@@ -242,7 +298,7 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button size="sm" onClick={addModule} disabled={newColumns.filter(c => c.label.trim()).length === 0} className="w-full">
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export function TableModule({ projectId, module, canEdit, notifyChanged }: Table
|
|||||||
const [draft, setDraft] = useState<Record<string, string>>({})
|
const [draft, setDraft] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
const columns: ModuleColumn[] = module.columns || []
|
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 () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -116,7 +117,7 @@ export function TableModule({ projectId, module, canEdit, notifyChanged }: Table
|
|||||||
|
|
||||||
// Grid-Template aus Spalten ableiten (+ Lösch-Spalte)
|
// Grid-Template aus Spalten ableiten (+ Lösch-Spalte)
|
||||||
const gridCols = columns.map(c =>
|
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' : '')
|
).join(' ') + (canEdit ? ' 24px' : '')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -167,6 +168,13 @@ export function TableModule({ projectId, module, canEdit, notifyChanged }: Table
|
|||||||
</div>
|
</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 (
|
return (
|
||||||
<div key={c.key} className="px-1.5 py-1.5 break-words min-w-0">
|
<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>}
|
{item.data[c.key] || <span className="text-muted-foreground">–</span>}
|
||||||
@@ -193,10 +201,11 @@ export function TableModule({ projectId, module, canEdit, notifyChanged }: Table
|
|||||||
<Input
|
<Input
|
||||||
key={c.key}
|
key={c.key}
|
||||||
placeholder={c.label + '…'}
|
placeholder={c.label + '…'}
|
||||||
|
inputMode={c.type === 'number' ? 'numeric' : undefined}
|
||||||
value={draft[c.key] || ''}
|
value={draft[c.key] || ''}
|
||||||
onChange={(e) => setDraft(prev => ({ ...prev, [c.key]: e.target.value }))}
|
onChange={(e) => setDraft(prev => ({ ...prev, [c.key]: e.target.value }))}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && addRow()}
|
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">
|
<Button size="sm" variant="ghost" onClick={addRow} disabled={!textColumns.some(c => (draft[c.key] || '').trim())} className="h-6 px-1.5 shrink-0">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* Ohne Konfiguration gelten die DEFAULT_MODULES (heutiges Verhalten).
|
* Ohne Konfiguration gelten die DEFAULT_MODULES (heutiges Verhalten).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ModuleColumnType = 'text' | 'check' | 'time'
|
export type ModuleColumnType = 'text' | 'number' | 'check' | 'time'
|
||||||
|
|
||||||
export interface ModuleColumn {
|
export interface ModuleColumn {
|
||||||
key: string
|
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: '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',
|
key: 'leer',
|
||||||
name: 'Eigenes Modul',
|
name: 'Eigenes Modul',
|
||||||
@@ -135,7 +150,7 @@ export function normalizeModules(raw: unknown): CockpitModule[] {
|
|||||||
? (m.columns as any[]).slice(0, 8).map((c, i) => ({
|
? (m.columns as any[]).slice(0, 8).map((c, i) => ({
|
||||||
key: typeof c.key === 'string' && c.key ? c.key : `col${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}`,
|
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,
|
width: typeof c.width === 'string' ? c.width : undefined,
|
||||||
}))
|
}))
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user