feat(rapport): eigene Cockpit-Module erscheinen im Einsatzrapport (v1.5.5)

- Rapport öffnen sammelt jetzt auch die Daten der eigenen Tabellen-Module
  (Atemschutz, Kräfte vor Ort, Lagemeldungen, eigene Checklisten)
- Neue Rapport-Sektion rendert jede Modul-Tabelle mit ihren Spalten
  (Text / Haken / Zeit) im PDF unter Pendenzen
- Vorschau der Module auch im Rapport-Dialog
- Zeit-Spalten: ISO → HH:MM, Haken → ✓/—

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-22 19:50:02 +02:00
parent bbf6fbdd13
commit 0a6c9cbcf2
4 changed files with 133 additions and 47 deletions

View File

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

View File

@@ -260,6 +260,38 @@ export default function RapportViewerPage({ params }: { params: Promise<{ token:
</Section>
)}
{/* 6b. Eigene Cockpit-Module (Modul-Baukasten) */}
{Array.isArray(d.moduleTables) && d.moduleTables.map((mt: any, ti: number) => (
Array.isArray(mt.rows) && mt.rows.length > 0 ? (
<Section key={`mod-${ti}`} num="•" title={mt.name}>
<table className="w-full border-collapse border rounded text-xs">
<thead>
<tr className="bg-gray-900 text-white">
{(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>
))}
</tr>
</thead>
<tbody>
{mt.rows.map((row: any, ri: number) => (
<tr key={ri} className={ri % 2 === 1 ? 'bg-gray-50' : ''}>
{(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' : ''}`}>
{c.type === 'check'
? (row[c.key] ? '✓' : '—')
: c.type === 'time'
? formatCellTime(row[c.key] || row._createdAt)
: (row[c.key] ?? '')}
</td>
))}
</tr>
))}
</tbody>
</table>
</Section>
) : null
))}
{/* 7. Eingesetzte Mittel */}
{d.fahrzeuge?.length > 0 && (
<Section num="7" title="Eingesetzte Mittel">
@@ -330,6 +362,16 @@ export default function RapportViewerPage({ params }: { params: Promise<{ token:
)
}
// Zeit-Zellen der Modul-Tabellen: ISO-Datum → HH:MM, sonst Rohwert
function formatCellTime(value: any): string {
if (!value) return ''
const d = new Date(value)
if (!isNaN(d.getTime()) && typeof value === 'string' && value.includes('T')) {
return d.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' })
}
return String(value)
}
function Section({ num, title, children }: { num: string; title: string; children: React.ReactNode }) {
return (
<div className="mb-4">

View File

@@ -414,6 +414,75 @@ export function JournalView({ projectId, projectTitle, projectLocation, mode, ei
window.print()
}, [])
// Rapport öffnen: Journal + SOMA + Pendenzen UND die eigenen Tabellen-Module
// einsammeln, damit alles im PDF-Rapport erscheint.
const [rapportLoading, setRapportLoading] = useState(false)
const handleOpenRapport = useCallback(async () => {
if (!projectId) return
setRapportLoading(true)
try {
// Daten der aktiven eigenen Module (type 'table') laden
const tableModules = modules.filter(m => m.enabled && m.type === 'table' && m.columns?.length)
const moduleTables = await Promise.all(tableModules.map(async (mod) => {
try {
const res = await fetch(`/api/projects/${projectId}/modules/${mod.id}/items`)
const data = res.ok ? await res.json() : { items: [] }
return {
name: mod.name,
columns: mod.columns,
rows: (data.items || []).map((it: any) => ({ ...it.data, _createdAt: it.createdAt })),
}
} catch {
return { name: mod.name, columns: mod.columns, rows: [] }
}
}))
const now = new Date()
setRapportForm({
organisation: tenantName || '',
abteilung: '',
datum: now.toLocaleDateString('de-CH'),
uhrzeit: now.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' }),
einsatzNr: einsatzNr || '',
alarmzeit: entries.length > 0 ? formatTime(entries[0].time) : '',
prioritaet: '',
einsatzort: projectLocation || '',
koordinaten: '',
objekt: '',
alarmierungsart: '',
stichwort: projectTitle || '',
zeitAlarm: entries.length > 0 ? formatTime(entries[0].time) : '',
zeitAusruecken: '', zeitEintreffen: '', zeitBereit: '',
zeitKontrolle: '', zeitAus: '', zeitEinruecken: '', zeitEnde: '',
lageEintreffen: '',
massnahmen: entries.map(e => `${formatTime(e.time)} ${e.what}${e.who ? ` (${e.who})` : ''}`),
somaItems: checkItems.map(c => ({
label: c.label,
confirmed: c.confirmed,
ok: c.ok,
confirmedAt: c.confirmedAt ? formatTime(c.confirmedAt) : null,
})),
pendenzenItems: pendenzen.map(p => ({
what: p.what,
who: p.who || '',
whenHow: p.whenHow || '',
done: p.done,
doneAt: p.doneAt ? formatTime(p.doneAt) : null,
})),
moduleTables,
fahrzeuge: [] as any[],
bemerkungen: '',
einsatzleiter: einsatzleiter || '',
rapporteur: journalfuehrer || '',
reportNumber: '',
logoUrl: tenantLogoUrl || '',
})
setShowRapportDialog(true)
} finally {
setRapportLoading(false)
}
}, [projectId, modules, tenantName, einsatzNr, entries, projectLocation, projectTitle, checkItems, pendenzen, einsatzleiter, journalfuehrer, tenantLogoUrl])
if (!projectId) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground">
@@ -456,53 +525,10 @@ export function JournalView({ projectId, projectTitle, projectLocation, mode, ei
<Button
variant="outline"
size="sm"
disabled={!projectId}
onClick={() => {
if (!projectId) return
const now = new Date()
// Pre-fill form with available data
setRapportForm({
organisation: tenantName || '',
abteilung: '',
datum: now.toLocaleDateString('de-CH'),
uhrzeit: now.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' }),
einsatzNr: einsatzNr || '',
alarmzeit: entries.length > 0 ? formatTime(entries[0].time) : '',
prioritaet: '',
einsatzort: projectLocation || '',
koordinaten: '',
objekt: '',
alarmierungsart: '',
stichwort: projectTitle || '',
zeitAlarm: entries.length > 0 ? formatTime(entries[0].time) : '',
zeitAusruecken: '', zeitEintreffen: '', zeitBereit: '',
zeitKontrolle: '', zeitAus: '', zeitEinruecken: '', zeitEnde: '',
lageEintreffen: '',
massnahmen: entries.map(e => `${formatTime(e.time)} ${e.what}${e.who ? ` (${e.who})` : ''}`),
somaItems: checkItems.map(c => ({
label: c.label,
confirmed: c.confirmed,
ok: c.ok,
confirmedAt: c.confirmedAt ? formatTime(c.confirmedAt) : null,
})),
pendenzenItems: pendenzen.map(p => ({
what: p.what,
who: p.who || '',
whenHow: p.whenHow || '',
done: p.done,
doneAt: p.doneAt ? formatTime(p.doneAt) : null,
})),
fahrzeuge: [] as any[],
bemerkungen: '',
einsatzleiter: einsatzleiter || '',
rapporteur: journalfuehrer || '',
reportNumber: '',
logoUrl: tenantLogoUrl || '',
})
setShowRapportDialog(true)
}}
disabled={!projectId || rapportLoading}
onClick={handleOpenRapport}
>
<FileText className="w-4 h-4 mr-1.5" />
{rapportLoading ? <Loader2 className="w-4 h-4 mr-1.5 animate-spin" /> : <FileText className="w-4 h-4 mr-1.5" />}
Rapport
</Button>
<Button variant="outline" size="sm" onClick={() => setShowSendDialog(!showSendDialog)}>

View File

@@ -235,6 +235,24 @@ export function RapportDialog({
</div>
</div>
)}
{/* Eigene Cockpit-Module (read-only, aus dem Journal) */}
{Array.isArray(rapportForm.moduleTables) && rapportForm.moduleTables.filter((mt: any) => mt.rows?.length > 0).map((mt: any, i: number) => (
<div key={i}>
<label className="text-xs font-semibold text-muted-foreground uppercase">{mt.name}</label>
<div className="border rounded-md p-2 bg-muted text-sm max-h-32 overflow-auto">
{mt.rows.map((row: any, ri: number) => (
<div key={ri} className="py-0.5 flex flex-wrap gap-x-3 text-xs">
{(mt.columns || []).filter((c: any) => c.type !== 'time').map((c: any) => (
<span key={c.key} className="text-muted-foreground">
<span className="font-medium text-foreground">{c.label}:</span>{' '}
{c.type === 'check' ? (row[c.key] ? '✓' : '—') : (row[c.key] || '—')}
</span>
))}
</div>
))}
</div>
</div>
))}
{/* Bemerkungen */}
<div>
<label className="text-xs font-semibold text-muted-foreground uppercase">Bemerkungen</label>