fix(cockpit): Modul-Spalten sauber ausgerichtet + Zahl-Spalten im Rapport (v1.8.8)

- table-module: Spaltenbreiten respektieren Breiten-Hinweise (w-16…w-32); Eingabezeile im
  selben Raster wie Kopf/Daten (keine abgeschnittenen Platzhalter, saubere Ausrichtung)
- Rapport (/rapport/[token]): Zahl-Spalten rechtsbündig/monospace
- Hinweis: Modul→Rapport war bereits vollständig integriert; leere Module werden im
  Rapport ausgelassen (erst mit Zeilen sichtbar)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-23 22:04:08 +02:00
parent 4a59b5ca0b
commit 2323f1d360
3 changed files with 33 additions and 20 deletions

View File

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

View File

@@ -268,7 +268,7 @@ export default function RapportViewerPage({ params }: { params: Promise<{ token:
<thead> <thead>
<tr className="bg-gray-900 text-white"> <tr className="bg-gray-900 text-white">
{(mt.columns || []).map((c: any) => ( {(mt.columns || []).map((c: any) => (
<th key={c.key} className={`p-1.5 font-semibold uppercase tracking-wider text-[7pt] ${c.type === 'check' ? 'text-center w-10' : c.type === 'time' ? 'text-left w-16' : 'text-left'}`}>{c.label}</th> <th key={c.key} className={`p-1.5 font-semibold uppercase tracking-wider text-[7pt] ${c.type === 'check' ? 'text-center w-10' : c.type === 'time' ? 'text-left w-16' : c.type === 'number' ? 'text-right w-20' : 'text-left'}`}>{c.label}</th>
))} ))}
</tr> </tr>
</thead> </thead>
@@ -276,7 +276,7 @@ export default function RapportViewerPage({ params }: { params: Promise<{ token:
{mt.rows.map((row: any, ri: number) => ( {mt.rows.map((row: any, ri: number) => (
<tr key={ri} className={ri % 2 === 1 ? 'bg-gray-50' : ''}> <tr key={ri} className={ri % 2 === 1 ? 'bg-gray-50' : ''}>
{(mt.columns || []).map((c: any) => ( {(mt.columns || []).map((c: any) => (
<td key={c.key} className={`p-1.5 border-b border-gray-100 ${c.type === 'check' ? 'text-center font-bold' : ''} ${c.type === 'time' ? 'font-mono text-[8pt] text-gray-500' : ''}`}> <td key={c.key} className={`p-1.5 border-b border-gray-100 ${c.type === 'check' ? 'text-center font-bold' : ''} ${c.type === 'time' ? 'font-mono text-[8pt] text-gray-500' : ''} ${c.type === 'number' ? 'text-right font-mono tabular-nums' : ''}`}>
{c.type === 'check' {c.type === 'check'
? (row[c.key] ? '✓' : '—') ? (row[c.key] ? '✓' : '—')
: c.type === 'time' : c.type === 'time'

View File

@@ -115,10 +115,17 @@ export function TableModule({ projectId, module, canEdit, notifyChanged }: Table
} }
}, [projectId, module.id, notifyChanged]) }, [projectId, module.id, notifyChanged])
// Spaltenbreite ableiten — respektiert optionale Breiten-Hinweise (w-16 … w-32)
const WIDTH_MAP: Record<string, string> = { 'w-10': '40px', 'w-16': '68px', 'w-20': '92px', 'w-24': '108px', 'w-28': '124px', 'w-32': '140px' }
const colSize = (c: ModuleColumn): string => {
if (c.type === 'check') return '36px'
if (c.type === 'time') return '58px'
if (c.width && WIDTH_MAP[c.width]) return WIDTH_MAP[c.width]
if (c.type === 'number') return '96px'
return 'minmax(90px,1fr)'
}
// 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(colSize).join(' ') + (canEdit ? ' 28px' : '')
c.type === 'check' ? '32px' : c.type === 'time' ? '52px' : c.type === 'number' ? 'minmax(0,0.6fr)' : 'minmax(0,1fr)'
).join(' ') + (canEdit ? ' 24px' : '')
return ( return (
<div> <div>
@@ -194,21 +201,27 @@ export function TableModule({ projectId, module, canEdit, notifyChanged }: Table
</div> </div>
)} )}
{/* Neue Zeile */} {/* Neue Zeile — im selben Raster wie die Spalten, damit Eingaben unter den Titeln stehen */}
{canEdit && ( {canEdit && (
<div className="flex gap-1 px-1.5 py-1.5 border-t border-border/50 print:hidden"> <div className="grid items-center gap-1 px-1.5 py-1.5 border-t border-border/50 print:hidden" style={{ gridTemplateColumns: gridCols }}>
{textColumns.map((c, i) => ( {columns.map(c => {
<Input if (c.type === 'text' || c.type === 'number') {
key={c.key} return (
placeholder={c.label + '…'} <Input
inputMode={c.type === 'number' ? 'numeric' : undefined} key={c.key}
value={draft[c.key] || ''} placeholder={c.label}
onChange={(e) => setDraft(prev => ({ ...prev, [c.key]: e.target.value }))} inputMode={c.type === 'number' ? 'numeric' : undefined}
onKeyDown={(e) => e.key === 'Enter' && addRow()} value={draft[c.key] || ''}
className={`h-6 text-[11px] ${i === 0 && c.type !== 'number' ? 'flex-1 min-w-0' : 'w-20 shrink-0'}`} onChange={(e) => setDraft(prev => ({ ...prev, [c.key]: e.target.value }))}
/> onKeyDown={(e) => e.key === 'Enter' && addRow()}
))} className="h-6 text-[11px] min-w-0 px-1.5"
<Button size="sm" variant="ghost" onClick={addRow} disabled={!textColumns.some(c => (draft[c.key] || '').trim())} className="h-6 px-1.5 shrink-0"> />
)
}
// Zeit/Haken: leere Zelle (wird automatisch/durch Klick gesetzt)
return <div key={c.key} className="text-center text-[10px] text-muted-foreground/50">{c.type === 'time' ? 'auto' : ''}</div>
})}
<Button size="sm" variant="ghost" onClick={addRow} disabled={!textColumns.some(c => (draft[c.key] || '').trim())} className="h-6 w-6 p-0 shrink-0">
<Plus className="w-3 h-3" /> <Plus className="w-3 h-3" />
</Button> </Button>
</div> </div>