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:
@@ -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": {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user