v1.0.7: fix symbol scale bug, custom layer dropdown, simplify rapport form, app logo in rapport

This commit is contained in:
Pepe Ziberi
2026-02-22 01:09:33 +01:00
parent e4c3c92cab
commit 0784553017
5 changed files with 81 additions and 91 deletions

View File

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

View File

@@ -145,9 +145,7 @@ export default function RapportViewerPage({ params }: { params: Promise<{ token:
{/* Header */} {/* Header */}
<div className="flex justify-between items-start pb-3 border-b-[3px] border-gray-900 mb-1"> <div className="flex justify-between items-start pb-3 border-b-[3px] border-gray-900 mb-1">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{d.logoUrl && ( <img src="/logo.svg" alt="Lageplan" className="w-10 h-10 object-contain" />
<img src={d.logoUrl} alt="Logo" className="w-10 h-10 object-contain" />
)}
<div> <div>
<h1 className="text-2xl font-bold tracking-tight">Einsatzrapport</h1> <h1 className="text-2xl font-bold tracking-tight">Einsatzrapport</h1>
<p className="text-xs text-gray-500 mt-0.5 font-medium">{d.organisation} · {d.abteilung}</p> <p className="text-xs text-gray-500 mt-0.5 font-medium">{d.organisation} · {d.abteilung}</p>
@@ -168,8 +166,7 @@ export default function RapportViewerPage({ params }: { params: Promise<{ token:
<Field label="Alarmzeit" value={d.alarmzeit} mono /> <Field label="Alarmzeit" value={d.alarmzeit} mono />
<Field label="Priorität" value={d.prioritaet} last /> <Field label="Priorität" value={d.prioritaet} last />
<Field label="Einsatzort / Adresse" value={d.einsatzort} span={2} /> <Field label="Einsatzort / Adresse" value={d.einsatzort} span={2} />
<Field label="Koordinaten" value={d.koordinaten} mono /> <Field label="Objekt / Gebäude" value={d.objekt} span={2} last />
<Field label="Objekt / Gebäude" value={d.objekt} last />
<Field label="Alarmierungsart" value={d.alarmierungsart} span={2} /> <Field label="Alarmierungsart" value={d.alarmierungsart} span={2} />
<Field label="Stichwort / Meldebild" value={d.stichwort} span={2} last /> <Field label="Stichwort / Meldebild" value={d.stichwort} span={2} last />
</div> </div>
@@ -177,15 +174,9 @@ export default function RapportViewerPage({ params }: { params: Promise<{ token:
{/* 2. Zeitverlauf */} {/* 2. Zeitverlauf */}
<Section num="2" title="Zeitverlauf"> <Section num="2" title="Zeitverlauf">
<div className="grid grid-cols-4 border rounded"> <div className="grid grid-cols-2 border rounded">
<Field label="Alarmierung" value={d.zeitAlarm} mono highlight /> <Field label="Alarmierung" value={d.zeitAlarm} mono highlight />
<Field label="Ausrücken" value={d.zeitAusruecken} mono highlight /> <Field label="Eintreffen" value={d.zeitEintreffen} mono highlight last />
<Field label="Eintreffen" value={d.zeitEintreffen} mono highlight />
<Field label="Einsatzbereit" value={d.zeitBereit} mono highlight last />
<Field label="Feuer unter Kontrolle" value={d.zeitKontrolle} mono highlight />
<Field label="Feuer aus" value={d.zeitAus} mono highlight />
<Field label="Einrücken" value={d.zeitEinruecken} mono highlight />
<Field label="Einsatzende" value={d.zeitEnde} mono highlight last />
</div> </div>
</Section> </Section>

View File

@@ -948,10 +948,6 @@ export function JournalView({ projectId, projectTitle, projectLocation, einsatzl
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-gray-500 uppercase">Koordinaten</label>
<Input value={rapportForm.koordinaten || ''} onChange={e => setRapportForm(f => ({ ...f, koordinaten: e.target.value }))} />
</div>
<div> <div>
<label className="text-xs font-semibold text-gray-500 uppercase">Alarmierungsart</label> <label className="text-xs font-semibold text-gray-500 uppercase">Alarmierungsart</label>
<Input value={rapportForm.alarmierungsart || ''} onChange={e => setRapportForm(f => ({ ...f, alarmierungsart: e.target.value }))} /> <Input value={rapportForm.alarmierungsart || ''} onChange={e => setRapportForm(f => ({ ...f, alarmierungsart: e.target.value }))} />
@@ -964,22 +960,15 @@ export function JournalView({ projectId, projectTitle, projectLocation, einsatzl
{/* Zeitverlauf */} {/* Zeitverlauf */}
<div> <div>
<label className="text-xs font-semibold text-gray-500 uppercase mb-1 block">Zeitverlauf</label> <label className="text-xs font-semibold text-gray-500 uppercase mb-1 block">Zeitverlauf</label>
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-2 gap-3">
{[ <div>
['zeitAlarm', 'Alarm'], <label className="text-[10px] text-gray-400">Alarm</label>
['zeitAusruecken', 'Ausrücken'], <Input type="time" className="text-sm h-8" value={rapportForm.zeitAlarm || ''} onChange={e => setRapportForm(f => ({ ...f, zeitAlarm: e.target.value }))} />
['zeitEintreffen', 'Eintreffen'], </div>
['zeitBereit', 'Bereit'], <div>
['zeitKontrolle', 'F. u. Kontrolle'], <label className="text-[10px] text-gray-400">Eintreffen</label>
['zeitAus', 'F. aus'], <Input type="time" className="text-sm h-8" value={rapportForm.zeitEintreffen || ''} onChange={e => setRapportForm(f => ({ ...f, zeitEintreffen: e.target.value }))} />
['zeitEinruecken', 'Einrücken'],
['zeitEnde', 'Ende'],
].map(([key, label]) => (
<div key={key}>
<label className="text-[10px] text-gray-400">{label}</label>
<Input className="text-sm h-8" value={rapportForm[key] || ''} onChange={e => setRapportForm(f => ({ ...f, [key]: e.target.value }))} placeholder="HH:MM" />
</div> </div>
))}
</div> </div>
</div> </div>
{/* Lagebild */} {/* Lagebild */}
@@ -1064,11 +1053,10 @@ export function JournalView({ projectId, projectTitle, projectLocation, einsatzl
} else { } else {
mapScreenshot = rawScreenshot mapScreenshot = rawScreenshot
} }
// Convert logo URL to base64 for PDF rendering // Convert Lageplan app logo to base64 for PDF rendering
let logoDataUri = '' let logoDataUri = ''
if (rapportForm.logoUrl) {
try { try {
const logoRes = await fetch(rapportForm.logoUrl) const logoRes = await fetch('/logo-icon.png')
if (logoRes.ok) { if (logoRes.ok) {
const blob = await logoRes.blob() const blob = await logoRes.blob()
logoDataUri = await new Promise<string>((resolve) => { logoDataUri = await new Promise<string>((resolve) => {
@@ -1078,8 +1066,7 @@ export function JournalView({ projectId, projectTitle, projectLocation, einsatzl
}) })
} }
} catch (e) { console.warn('Logo fetch failed:', e) } } catch (e) { console.warn('Logo fetch failed:', e) }
} const rapportData = { ...rapportForm, mapScreenshot, logoUrl: logoDataUri || '/logo-icon.png' }
const rapportData = { ...rapportForm, mapScreenshot, logoUrl: logoDataUri || rapportForm.logoUrl }
console.log('[Rapport] Sending request, body size ~', JSON.stringify({ projectId, data: rapportData }).length, 'bytes') console.log('[Rapport] Sending request, body size ~', JSON.stringify({ projectId, data: rapportData }).length, 'bytes')
const res = await fetch('/api/rapports', { const res = await fetch('/api/rapports', {
method: 'POST', method: 'POST',

View File

@@ -107,6 +107,7 @@ export function MapView({
const measureCoordsRef = useRef<number[][]>([]) const measureCoordsRef = useRef<number[][]>([])
const [isMapLoaded, setIsMapLoaded] = useState(false) const [isMapLoaded, setIsMapLoaded] = useState(false)
const [activeBaseLayer, setActiveBaseLayer] = useState<'osm' | 'satellite' | 'swisstopo' | 'swissimage'>('osm') const [activeBaseLayer, setActiveBaseLayer] = useState<'osm' | 'satellite' | 'swisstopo' | 'swissimage'>('osm')
const [layerDropdownOpen, setLayerDropdownOpen] = useState(false)
const [measurePointCount, setMeasurePointCount] = useState(0) const [measurePointCount, setMeasurePointCount] = useState(0)
const [measureFinished, setMeasureFinished] = useState(false) const [measureFinished, setMeasureFinished] = useState(false)
const [drawingPointCount, setDrawingPointCount] = useState(0) const [drawingPointCount, setDrawingPointCount] = useState(0)
@@ -2106,11 +2107,12 @@ export function MapView({
selectedSymbolRef.current.scale = Math.max(0.2, Math.min(10, startScale * ratio)) selectedSymbolRef.current.scale = Math.max(0.2, Math.min(10, startScale * ratio))
selectedSymbolRef.current.innerEl.style.fontSize = `${baseFontSize * selectedSymbolRef.current.scale}px` selectedSymbolRef.current.innerEl.style.fontSize = `${baseFontSize * selectedSymbolRef.current.scale}px`
} else { } else {
// For symbols: resize wrapper // For symbols: resize wrapper, use ratio from start to preserve zoom-aware scale
selectedSymbolRef.current.wrapperEl.style.width = `${width}px` selectedSymbolRef.current.wrapperEl.style.width = `${width}px`
selectedSymbolRef.current.wrapperEl.style.height = `${height}px` selectedSymbolRef.current.wrapperEl.style.height = `${height}px`
const baseSize = 32 const startW = selectedSymbolRef.current.resizeStartWidth || 1
selectedSymbolRef.current.scale = Math.max(0.1, Math.min(10, width / baseSize)) const startScale = selectedSymbolRef.current.resizeStartScale || 1
selectedSymbolRef.current.scale = Math.max(0.1, Math.min(10, startScale * (width / startW)))
} }
} }
}} }}
@@ -2155,33 +2157,52 @@ export function MapView({
)} )}
{/* Layer selector dropdown */} {/* Layer selector dropdown */}
<select <div className="absolute top-3 right-3 z-10">
value={activeBaseLayer} <button
onChange={(e) => { onClick={() => setLayerDropdownOpen(v => !v)}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-[11px] font-medium shadow-md border backdrop-blur-sm transition-all"
style={{
background: activeBaseLayer !== 'osm' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.92)',
color: activeBaseLayer !== 'osm' ? '#fff' : '#1f2937',
borderColor: activeBaseLayer !== 'osm' ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)',
}}
>
<svg className="w-3.5 h-3.5 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
{{ osm: 'OpenStreetMap', satellite: 'Satellit', swisstopo: 'Swisstopo', swissimage: 'Luftbild CH' }[activeBaseLayer]}
<svg className={`w-3 h-3 opacity-50 transition-transform ${layerDropdownOpen ? 'rotate-180' : ''}`} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M6 9l6 6 6-6"/></svg>
</button>
{layerDropdownOpen && (
<>
<div className="fixed inset-0 z-10" onClick={() => setLayerDropdownOpen(false)} />
<div className="absolute right-0 mt-1 z-20 min-w-[160px] rounded-lg shadow-xl border overflow-hidden backdrop-blur-md"
style={{ background: 'rgba(255,255,255,0.95)', borderColor: 'rgba(0,0,0,0.08)' }}>
{([
{ key: 'osm', label: 'OpenStreetMap' },
{ key: 'satellite', label: 'Satellit (Esri)' },
{ key: 'swisstopo', label: 'Swisstopo Karte' },
{ key: 'swissimage', label: 'Luftbild CH' },
] as const).map(({ key, label }) => (
<button
key={key}
onClick={() => {
if (!map.current) return if (!map.current) return
const newLayer = e.target.value as 'osm' | 'satellite' | 'swisstopo' | 'swissimage'
const allLayers: Array<'osm' | 'satellite' | 'swisstopo' | 'swissimage'> = ['osm', 'satellite', 'swisstopo', 'swissimage'] const allLayers: Array<'osm' | 'satellite' | 'swisstopo' | 'swissimage'> = ['osm', 'satellite', 'swisstopo', 'swissimage']
for (const l of allLayers) { for (const l of allLayers) {
map.current.setLayoutProperty(l, 'visibility', l === newLayer ? 'visible' : 'none') map.current.setLayoutProperty(l, 'visibility', l === key ? 'visible' : 'none')
} }
setActiveBaseLayer(newLayer) setActiveBaseLayer(key)
setLayerDropdownOpen(false)
}} }}
className="absolute top-3 right-3 z-10 px-2.5 py-1.5 rounded-lg text-xs font-semibold shadow-lg border transition-colors cursor-pointer appearance-none pr-7" className={`w-full text-left px-3 py-2 text-[11px] font-medium transition-colors ${activeBaseLayer === key ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'}`}
style={{
background: activeBaseLayer !== 'osm' ? 'rgba(0,0,0,0.75)' : 'rgba(255,255,255,0.95)',
color: activeBaseLayer !== 'osm' ? '#fff' : '#333',
borderColor: activeBaseLayer !== 'osm' ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.15)',
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='${activeBaseLayer !== 'osm' ? 'white' : '%23333'}' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 6px center',
}}
title="Kartenstil wählen"
> >
<option value="osm">🗺 OpenStreetMap</option> {activeBaseLayer === key && <span className="inline-block w-1.5 h-1.5 rounded-full bg-blue-500 mr-2 align-middle" />}
<option value="satellite">🛰 Satellit (Esri)</option> {label}
<option value="swisstopo">🇨🇭 Swisstopo Karte</option> </button>
<option value="swissimage">🇨🇭 Swisstopo Luftbild</option> ))}
</select> </div>
</>
)}
</div>
{/* Zeichnung abschliessen Button (Linie/Polygon/Pfeil) */} {/* Zeichnung abschliessen Button (Linie/Polygon/Pfeil) */}
{(drawMode === 'linestring' || drawMode === 'polygon' || drawMode === 'arrow' || drawMode === 'dangerzone') && drawingPointCount >= 2 && ( {(drawMode === 'linestring' || drawMode === 'polygon' || drawMode === 'arrow' || drawMode === 'dangerzone') && drawingPointCount >= 2 && (

View File

@@ -166,8 +166,7 @@ export function RapportDocument({ data }: { data: RapportData }) {
</View> </View>
<View style={styles.fieldRow}> <View style={styles.fieldRow}>
<FieldCell label="Einsatzort / Adresse" value={data.einsatzort} width="50%" /> <FieldCell label="Einsatzort / Adresse" value={data.einsatzort} width="50%" />
<FieldCell label="Koordinaten" value={data.koordinaten} mono width="25%" /> <FieldCell label="Objekt / Gebäude" value={data.objekt} width="50%" />
<FieldCell label="Objekt / Gebäude" value={data.objekt} width="25%" />
</View> </View>
<View style={styles.fieldRow}> <View style={styles.fieldRow}>
<FieldCell label="Alarmierungsart" value={data.alarmierungsart} width="50%" /> <FieldCell label="Alarmierungsart" value={data.alarmierungsart} width="50%" />
@@ -185,16 +184,8 @@ export function RapportDocument({ data }: { data: RapportData }) {
</View> </View>
<View style={styles.fieldGrid}> <View style={styles.fieldGrid}>
<View style={styles.fieldRow}> <View style={styles.fieldRow}>
<FieldCell label="Alarmierung" value={data.zeitAlarm} mono highlight width="25%" /> <FieldCell label="Alarmierung" value={data.zeitAlarm} mono highlight width="50%" />
<FieldCell label="Ausrücken" value={data.zeitAusruecken} mono highlight width="25%" /> <FieldCell label="Eintreffen" value={data.zeitEintreffen} mono highlight width="50%" />
<FieldCell label="Eintreffen" value={data.zeitEintreffen} mono highlight width="25%" />
<FieldCell label="Einsatzbereit" value={data.zeitBereit} mono highlight width="25%" />
</View>
<View style={styles.fieldRow}>
<FieldCell label="Feuer unter Kontrolle" value={data.zeitKontrolle} mono highlight width="25%" />
<FieldCell label="Feuer aus" value={data.zeitAus} mono highlight width="25%" />
<FieldCell label="Einrücken" value={data.zeitEinruecken} mono highlight width="25%" />
<FieldCell label="Einsatzende" value={data.zeitEnde} mono highlight width="25%" />
</View> </View>
</View> </View>
</View> </View>