fix(journal/rapport): SOMA atomar + Rapport-Korrektheit + Export-dpr (v1.6.5)
- SOMA-Init atomar: pg_advisory_xact_lock pro Projekt in Transaktion +
createMany → keine doppelten SOMA-Listen mehr, wenn zwei Geräte denselben
frischen Einsatz gleichzeitig öffnen. initCheckItems meldet Änderung an
andere Clients (journal-refresh).
- Rapport: korrigierte (durchgestrichene) Original-Einträge werden nicht mehr
als gültige Massnahmen übernommen; Erledigt-Status ("— erledigt HH:MM")
erscheint jetzt im Rapport; SOMA zeigt okAt, falls confirmedAt fehlt.
- Export: dpr-Fallback auf devicePixelRatio, wenn die Karte ausgeblendet ist
(Journal-Tab) — vorher Infinity → NaN-Grössen, Export defekt.
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.6.4",
|
"version": "1.6.5",
|
||||||
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -16,36 +16,33 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
|
|
||||||
const body = await req.json()
|
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) {
|
if (body.initFromTemplates) {
|
||||||
const existing = await (prisma as any).journalCheckItem.findMany({
|
const items = await (prisma as any).$transaction(async (tx: any) => {
|
||||||
where: { projectId: id },
|
// Serialisiert Init-Aufrufe desselben Projekts (bis Transaktionsende)
|
||||||
})
|
await tx.$executeRawUnsafe(`SELECT pg_advisory_xact_lock(hashtext($1))`, `checkitems:${id}`)
|
||||||
if (existing.length > 0) {
|
|
||||||
return NextResponse.json(existing)
|
const existing = await tx.journalCheckItem.findMany({ where: { projectId: id } })
|
||||||
}
|
if (existing.length > 0) return existing
|
||||||
// Prefer tenant-specific templates; fall back to global (tenantId=null) if none exist
|
|
||||||
let templates = await (prisma as any).journalCheckTemplate.findMany({
|
let templates = await tx.journalCheckTemplate.findMany({
|
||||||
where: { isActive: true, tenantId: user.tenantId || null },
|
where: { isActive: true, tenantId: user.tenantId || null },
|
||||||
orderBy: { sortOrder: 'asc' },
|
orderBy: { sortOrder: 'asc' },
|
||||||
})
|
})
|
||||||
if (templates.length === 0 && user.tenantId) {
|
if (templates.length === 0 && user.tenantId) {
|
||||||
templates = await (prisma as any).journalCheckTemplate.findMany({
|
templates = await tx.journalCheckTemplate.findMany({
|
||||||
where: { isActive: true, tenantId: null },
|
where: { isActive: true, tenantId: null },
|
||||||
orderBy: { sortOrder: 'asc' },
|
orderBy: { sortOrder: 'asc' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const items = await Promise.all(
|
if (templates.length === 0) return []
|
||||||
templates.map((tpl: any, i: number) =>
|
await tx.journalCheckItem.createMany({
|
||||||
(prisma as any).journalCheckItem.create({
|
data: templates.map((tpl: any, i: number) => ({ projectId: id, label: tpl.label, sortOrder: i })),
|
||||||
data: {
|
})
|
||||||
projectId: id,
|
return tx.journalCheckItem.findMany({ where: { projectId: id }, orderBy: { sortOrder: 'asc' } })
|
||||||
label: tpl.label,
|
|
||||||
sortOrder: i,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
)
|
|
||||||
)
|
|
||||||
return NextResponse.json(items)
|
return NextResponse.json(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export default function RapportViewerPage({ params }: { params: Promise<{ token:
|
|||||||
<span className="text-center font-bold">{s.confirmed ? '✓' : '—'}</span>
|
<span className="text-center font-bold">{s.confirmed ? '✓' : '—'}</span>
|
||||||
<span className="text-center font-bold">{s.ok ? '✓' : '—'}</span>
|
<span className="text-center font-bold">{s.ok ? '✓' : '—'}</span>
|
||||||
<span className="font-medium">{s.label}</span>
|
<span className="font-medium">{s.label}</span>
|
||||||
<span className="text-right text-[8pt] text-gray-500 font-mono">{s.confirmedAt || ''}</span>
|
<span className="text-right text-[8pt] text-gray-500 font-mono">{s.confirmedAt || s.okAt || ''}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -169,11 +169,12 @@ export function JournalView({ projectId, projectTitle, projectLocation, mode, ei
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const items = await res.json()
|
const items = await res.json()
|
||||||
setCheckItems(items)
|
setCheckItems(items)
|
||||||
|
notifyJournalChanged() // andere offene Clients über die neue SOMA-Liste informieren
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to init check items:', err)
|
console.error('Failed to init check items:', err)
|
||||||
}
|
}
|
||||||
}, [projectId])
|
}, [projectId, notifyJournalChanged])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initDoneRef.current = false
|
initDoneRef.current = false
|
||||||
@@ -455,12 +456,17 @@ export function JournalView({ projectId, projectTitle, projectLocation, mode, ei
|
|||||||
zeitAusruecken: '', zeitEintreffen: '', zeitBereit: '',
|
zeitAusruecken: '', zeitEintreffen: '', zeitBereit: '',
|
||||||
zeitKontrolle: '', zeitAus: '', zeitEinruecken: '', zeitEnde: '',
|
zeitKontrolle: '', zeitAus: '', zeitEinruecken: '', zeitEnde: '',
|
||||||
lageEintreffen: '',
|
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 => ({
|
somaItems: checkItems.map(c => ({
|
||||||
label: c.label,
|
label: c.label,
|
||||||
confirmed: c.confirmed,
|
confirmed: c.confirmed,
|
||||||
ok: c.ok,
|
ok: c.ok,
|
||||||
confirmedAt: c.confirmedAt ? formatTime(c.confirmedAt) : null,
|
confirmedAt: c.confirmedAt ? formatTime(c.confirmedAt) : null,
|
||||||
|
okAt: c.okAt ? formatTime(c.okAt) : null,
|
||||||
})),
|
})),
|
||||||
pendenzenItems: pendenzen.map(p => ({
|
pendenzenItems: pendenzen.map(p => ({
|
||||||
what: p.what,
|
what: p.what,
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ export function useMapExport({
|
|||||||
const currentFeatures = featuresRef.current
|
const currentFeatures = featuresRef.current
|
||||||
// Derive actual pixel ratio from canvas vs container (more reliable than window.devicePixelRatio)
|
// Derive actual pixel ratio from canvas vs container (more reliable than window.devicePixelRatio)
|
||||||
const container = mapInstance.getContainer()
|
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()
|
const zoom = mapInstance.getZoom()
|
||||||
// Symbol sizing: match the map rendering logic exactly
|
// Symbol sizing: match the map rendering logic exactly
|
||||||
// In map-view.tsx: size = baseSize * scale * Math.pow(2, currentZoom - placementZoom)
|
// In map-view.tsx: size = baseSize * scale * Math.pow(2, currentZoom - placementZoom)
|
||||||
|
|||||||
Reference in New Issue
Block a user