diff --git a/package.json b/package.json
index 00cffab..508461a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "lageplan",
- "version": "1.6.4",
+ "version": "1.6.5",
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
"private": true,
"scripts": {
diff --git a/src/app/api/projects/[id]/journal/check-items/route.ts b/src/app/api/projects/[id]/journal/check-items/route.ts
index 6f803a6..2c6eaff 100644
--- a/src/app/api/projects/[id]/journal/check-items/route.ts
+++ b/src/app/api/projects/[id]/journal/check-items/route.ts
@@ -16,36 +16,33 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
const body = await req.json()
- // If 'initFromTemplates' is true, create check items from templates (only if none exist)
+ // If 'initFromTemplates' is true, create check items from templates (only if none exist).
+ // ATOMAR: Advisory-Lock pro Projekt verhindert doppelte SOMA-Listen, wenn zwei Geräte
+ // denselben frischen Einsatz gleichzeitig öffnen (Client-Guard schützt nur pro Client).
if (body.initFromTemplates) {
- const existing = await (prisma as any).journalCheckItem.findMany({
- where: { projectId: id },
- })
- if (existing.length > 0) {
- return NextResponse.json(existing)
- }
- // Prefer tenant-specific templates; fall back to global (tenantId=null) if none exist
- let templates = await (prisma as any).journalCheckTemplate.findMany({
- where: { isActive: true, tenantId: user.tenantId || null },
- orderBy: { sortOrder: 'asc' },
- })
- if (templates.length === 0 && user.tenantId) {
- templates = await (prisma as any).journalCheckTemplate.findMany({
- where: { isActive: true, tenantId: null },
+ const items = await (prisma as any).$transaction(async (tx: any) => {
+ // Serialisiert Init-Aufrufe desselben Projekts (bis Transaktionsende)
+ await tx.$executeRawUnsafe(`SELECT pg_advisory_xact_lock(hashtext($1))`, `checkitems:${id}`)
+
+ const existing = await tx.journalCheckItem.findMany({ where: { projectId: id } })
+ if (existing.length > 0) return existing
+
+ let templates = await tx.journalCheckTemplate.findMany({
+ where: { isActive: true, tenantId: user.tenantId || null },
orderBy: { sortOrder: 'asc' },
})
- }
- const items = await Promise.all(
- templates.map((tpl: any, i: number) =>
- (prisma as any).journalCheckItem.create({
- data: {
- projectId: id,
- label: tpl.label,
- sortOrder: i,
- },
+ if (templates.length === 0 && user.tenantId) {
+ templates = await tx.journalCheckTemplate.findMany({
+ where: { isActive: true, tenantId: null },
+ orderBy: { sortOrder: 'asc' },
})
- )
- )
+ }
+ if (templates.length === 0) return []
+ await tx.journalCheckItem.createMany({
+ data: templates.map((tpl: any, i: number) => ({ projectId: id, label: tpl.label, sortOrder: i })),
+ })
+ return tx.journalCheckItem.findMany({ where: { projectId: id }, orderBy: { sortOrder: 'asc' } })
+ })
return NextResponse.json(items)
}
diff --git a/src/app/rapport/[token]/page.tsx b/src/app/rapport/[token]/page.tsx
index 7db092f..993f4ff 100644
--- a/src/app/rapport/[token]/page.tsx
+++ b/src/app/rapport/[token]/page.tsx
@@ -225,7 +225,7 @@ export default function RapportViewerPage({ params }: { params: Promise<{ token:
{s.confirmed ? '✓' : '—'}
{s.ok ? '✓' : '—'}
{s.label}
- {s.confirmedAt || ''}
+ {s.confirmedAt || s.okAt || ''}
))}
diff --git a/src/components/journal/journal-view.tsx b/src/components/journal/journal-view.tsx
index 7e11a6d..92698ee 100644
--- a/src/components/journal/journal-view.tsx
+++ b/src/components/journal/journal-view.tsx
@@ -169,11 +169,12 @@ export function JournalView({ projectId, projectTitle, projectLocation, mode, ei
if (res.ok) {
const items = await res.json()
setCheckItems(items)
+ notifyJournalChanged() // andere offene Clients über die neue SOMA-Liste informieren
}
} catch (err) {
console.error('Failed to init check items:', err)
}
- }, [projectId])
+ }, [projectId, notifyJournalChanged])
useEffect(() => {
initDoneRef.current = false
@@ -455,12 +456,17 @@ export function JournalView({ projectId, projectTitle, projectLocation, mode, ei
zeitAusruecken: '', zeitEintreffen: '', zeitBereit: '',
zeitKontrolle: '', zeitAus: '', zeitEinruecken: '', zeitEnde: '',
lageEintreffen: '',
- massnahmen: entries.map(e => `${formatTime(e.time)} ${e.what}${e.who ? ` (${e.who})` : ''}`),
+ // Nur gültige Massnahmen: durchgestrichene Original-Einträge (isCorrected)
+ // ausfiltern; Erledigt-Status übernehmen.
+ massnahmen: entries
+ .filter(e => !(e as any).isCorrected)
+ .map(e => `${formatTime(e.time)} ${e.what}${e.who ? ` (${e.who})` : ''}${e.done && e.doneAt ? ` — erledigt ${formatTime(e.doneAt)}` : ''}`),
somaItems: checkItems.map(c => ({
label: c.label,
confirmed: c.confirmed,
ok: c.ok,
confirmedAt: c.confirmedAt ? formatTime(c.confirmedAt) : null,
+ okAt: c.okAt ? formatTime(c.okAt) : null,
})),
pendenzenItems: pendenzen.map(p => ({
what: p.what,
diff --git a/src/hooks/use-map-export.ts b/src/hooks/use-map-export.ts
index 245f1a1..55aa3e0 100644
--- a/src/hooks/use-map-export.ts
+++ b/src/hooks/use-map-export.ts
@@ -43,7 +43,11 @@ export function useMapExport({
const currentFeatures = featuresRef.current
// Derive actual pixel ratio from canvas vs container (more reliable than window.devicePixelRatio)
const container = mapInstance.getContainer()
- const dpr = mapCanvas.width / container.offsetWidth
+ // Ist die Karte gerade ausgeblendet (Journal-Tab aktiv), ist offsetWidth 0 →
+ // dpr würde Infinity und alle Grössen NaN. Dann auf devicePixelRatio zurückfallen.
+ const dpr = container.offsetWidth > 0
+ ? mapCanvas.width / container.offsetWidth
+ : (typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1)
const zoom = mapInstance.getZoom()
// Symbol sizing: match the map rendering logic exactly
// In map-view.tsx: size = baseSize * scale * Math.pow(2, currentZoom - placementZoom)