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",
|
"name": "lageplan",
|
||||||
"version": "1.5.4",
|
"version": "1.5.5",
|
||||||
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -260,6 +260,38 @@ export default function RapportViewerPage({ params }: { params: Promise<{ token:
|
|||||||
</Section>
|
</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 */}
|
{/* 7. Eingesetzte Mittel */}
|
||||||
{d.fahrzeuge?.length > 0 && (
|
{d.fahrzeuge?.length > 0 && (
|
||||||
<Section num="7" title="Eingesetzte Mittel">
|
<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 }) {
|
function Section({ num, title, children }: { num: string; title: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|||||||
@@ -414,6 +414,75 @@ export function JournalView({ projectId, projectTitle, projectLocation, mode, ei
|
|||||||
window.print()
|
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) {
|
if (!projectId) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
<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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={!projectId}
|
disabled={!projectId || rapportLoading}
|
||||||
onClick={() => {
|
onClick={handleOpenRapport}
|
||||||
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)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<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
|
Rapport
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" onClick={() => setShowSendDialog(!showSendDialog)}>
|
<Button variant="outline" size="sm" onClick={() => setShowSendDialog(!showSendDialog)}>
|
||||||
|
|||||||
@@ -235,6 +235,24 @@ export function RapportDialog({
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Bemerkungen */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-semibold text-muted-foreground uppercase">Bemerkungen</label>
|
<label className="text-xs font-semibold text-muted-foreground uppercase">Bemerkungen</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user