feat(field): Symbole am Stück setzen + eigene GPS-Position (v1.5.4)

- Tap-to-place bleibt scharf: mehrere gleiche Symbole nacheinander setzen
  (z.B. 4 Hydranten), Hinweis zeigt Anzahl + "Fertig"-Knopf
- Zeichen-Werkzeug wählen beendet den Platzier-Modus
- GPS: eigene Position dauerhaft mit Genauigkeitskreis + Blickrichtung
- Beim Öffnen bleibt die Karte auf dem Einsatzort (springt nicht mehr auf
  die eigene Position, wenn ein Einsatzort gesetzt ist)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-22 19:46:43 +02:00
parent 03914083df
commit bbf6fbdd13
3 changed files with 47 additions and 15 deletions

View File

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

View File

@@ -19,7 +19,7 @@ import { useAuth } from '@/components/providers/auth-provider'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { JournalView } from '@/components/journal/journal-view' import { JournalView } from '@/components/journal/journal-view'
import { Lock, Eye, AlertTriangle, WifiOff, GraduationCap, Map as MapIcon, ClipboardList, LayoutGrid, X } from 'lucide-react' import { Lock, Eye, AlertTriangle, WifiOff, GraduationCap, Map as MapIcon, ClipboardList, LayoutGrid } from 'lucide-react'
import { CustomDragLayer } from '@/components/map/custom-drag-layer' import { CustomDragLayer } from '@/components/map/custom-drag-layer'
import { OnboardingTour, resetOnboardingTour } from '@/components/onboarding/onboarding-tour' import { OnboardingTour, resetOnboardingTour } from '@/components/onboarding/onboarding-tour'
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts' import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'
@@ -51,6 +51,8 @@ export default function AppPage() {
const [isEndConfirmOpen, setIsEndConfirmOpen] = useState(false) const [isEndConfirmOpen, setIsEndConfirmOpen] = useState(false)
// Tap-to-place: angetipptes Symbol wartet auf Platzierung per Karten-Tipp (Mobil) // Tap-to-place: angetipptes Symbol wartet auf Platzierung per Karten-Tipp (Mobil)
const [pendingSymbol, setPendingSymbol] = useState<{ id: string; imageUrl: string } | null>(null) const [pendingSymbol, setPendingSymbol] = useState<{ id: string; imageUrl: string } | null>(null)
// Feld-Tempo: bleibt scharf, um mehrere gleiche Symbole am Stück zu setzen
const [placedCount, setPlacedCount] = useState(0)
const [isSaving, setIsSaving] = useState(false) const [isSaving, setIsSaving] = useState(false)
const [isDeleteAllConfirmOpen, setIsDeleteAllConfirmOpen] = useState(false) const [isDeleteAllConfirmOpen, setIsDeleteAllConfirmOpen] = useState(false)
const [isFullscreen, setIsFullscreen] = useState(false) const [isFullscreen, setIsFullscreen] = useState(false)
@@ -403,7 +405,11 @@ export default function AppPage() {
// Werkzeugwechsel auf ein Zeichen-Tool übernimmt vorab den Lock (rechtzeitig vor dem Zeichnen) // Werkzeugwechsel auf ein Zeichen-Tool übernimmt vorab den Lock (rechtzeitig vor dem Zeichnen)
const handleDrawModeChange = useCallback((mode: DrawMode) => { const handleDrawModeChange = useCallback((mode: DrawMode) => {
if (mode !== 'select') ensureEditing() if (mode !== 'select') {
ensureEditing()
setPendingSymbol(null) // ein Zeichen-Werkzeug beendet das Symbol-Platzieren
setPlacedCount(0)
}
setDrawMode(mode) setDrawMode(mode)
}, [ensureEditing, setDrawMode]) }, [ensureEditing, setDrawMode])
@@ -769,10 +775,11 @@ export default function AppPage() {
const handleSymbolSelect = useCallback((symbol: { id: string; imageUrl: string }) => { const handleSymbolSelect = useCallback((symbol: { id: string; imageUrl: string }) => {
ensureEditing() // Lock schon beim Auswählen übernehmen, damit das Platzieren sofort greift ensureEditing() // Lock schon beim Auswählen übernehmen, damit das Platzieren sofort greift
setPendingSymbol(symbol) setPendingSymbol(symbol)
setPlacedCount(0)
setIsSidebarOpen(false) setIsSidebarOpen(false)
setActiveTab('map') setActiveTab('map')
setDrawMode('select') setDrawMode('select')
toast({ title: 'Symbol platzieren', description: 'Tippe auf die Karte, um das Symbol zu setzen.' }) toast({ title: 'Symbol platzieren', description: 'Auf die Karte tippen — mehrere möglich, dann „Fertig".' })
}, [setIsSidebarOpen, setActiveTab, toast, ensureEditing, setDrawMode]) }, [setIsSidebarOpen, setActiveTab, toast, ensureEditing, setDrawMode])
const handleTextPlace = useCallback((coordinates: [number, number]) => { const handleTextPlace = useCallback((coordinates: [number, number]) => {
@@ -1038,7 +1045,7 @@ export default function AppPage() {
onDrawModeChange={setDrawMode} onDrawModeChange={setDrawMode}
undoDrawPointRef={undoDrawPointRef} undoDrawPointRef={undoDrawPointRef}
pendingSymbol={pendingSymbol ? { iconId: pendingSymbol.id, imageUrl: pendingSymbol.imageUrl } : null} pendingSymbol={pendingSymbol ? { iconId: pendingSymbol.id, imageUrl: pendingSymbol.imageUrl } : null}
onSymbolPlaced={() => setPendingSymbol(null)} onSymbolPlaced={() => setPlacedCount((n) => n + 1)}
isFullscreen={isFullscreen} isFullscreen={isFullscreen}
onToggleFullscreen={toggleFullscreen} onToggleFullscreen={toggleFullscreen}
/> />
@@ -1093,13 +1100,20 @@ export default function AppPage() {
/> />
</div> </div>
{/* Tap-to-place Hinweis — schwebt über der Bottom-Nav, mit Abbrechen */} {/* Tap-to-place Hinweis — schwebt über der Bottom-Nav; mehrere setzen, dann „Fertig" */}
{pendingSymbol && ( {pendingSymbol && (
<div className="fixed left-1/2 -translate-x-1/2 z-40 bottom-[76px] md:bottom-6 flex items-center gap-2 px-3 py-2 rounded-full bg-primary text-primary-foreground shadow-lg text-sm"> <div className="fixed left-1/2 -translate-x-1/2 z-40 bottom-[76px] md:bottom-6 flex items-center gap-2 pl-3 pr-2 py-2 rounded-full bg-primary text-primary-foreground shadow-lg text-sm">
<img src={pendingSymbol.imageUrl} alt="" className="w-5 h-5 object-contain bg-white rounded" crossOrigin="anonymous" /> <img src={pendingSymbol.imageUrl} alt="" className="w-5 h-5 object-contain bg-white rounded" crossOrigin="anonymous" />
<span>Auf die Karte tippen zum Platzieren</span> <span>
<button onClick={() => setPendingSymbol(null)} className="ml-1 p-0.5 rounded-full hover:bg-white/20" title="Abbrechen"> {placedCount > 0
<X className="w-4 h-4" /> ? `Auf Karte tippen · ${placedCount} gesetzt`
: 'Auf die Karte tippen zum Platzieren'}
</span>
<button
onClick={() => { setPendingSymbol(null); setPlacedCount(0) }}
className="ml-1 px-2.5 py-1 rounded-full bg-white/20 hover:bg-white/30 text-xs font-semibold"
>
Fertig
</button> </button>
</div> </div>
)} )}

View File

@@ -706,16 +706,34 @@ export function MapView({
map.current.addControl(new maplibregl.NavigationControl(), 'bottom-right') map.current.addControl(new maplibregl.NavigationControl(), 'bottom-right')
map.current.addControl(new maplibregl.ScaleControl(), 'bottom-left') map.current.addControl(new maplibregl.ScaleControl(), 'bottom-left')
// Geolocation: center map on user's position // Geolocation: eigene Position dauerhaft anzeigen (Punkt + Genauigkeit + Blickrichtung)
const geolocate = new maplibregl.GeolocateControl({ const geolocate = new maplibregl.GeolocateControl({
positionOptions: { enableHighAccuracy: true }, positionOptions: { enableHighAccuracy: true },
trackUserLocation: true, trackUserLocation: true,
showAccuracyCircle: false, showAccuracyCircle: true,
}) // showUserHeading zeigt die Blickrichtung — in den Typen dieser Version (noch)
// nicht deklariert, zur Laufzeit aber vorhanden.
showUserHeading: true,
} as any)
map.current.addControl(geolocate, 'bottom-right') map.current.addControl(geolocate, 'bottom-right')
// Auto-trigger geolocation when map loads to center on user's real position // Position beim Laden aktivieren (Punkt erscheint), aber bei gesetztem Einsatzort
map.current.on('load', () => { geolocate.trigger() }) // auf dem Einsatzort bleiben statt auf die eigene Position zu springen.
const einsatzortCenter = project?.mapCenter
const hasEinsatzort = !!einsatzortCenter &&
!(einsatzortCenter.lng === 8.5417 && einsatzortCenter.lat === 47.3769)
map.current.on('load', () => {
geolocate.trigger()
if (hasEinsatzort && einsatzortCenter) {
geolocate.once('geolocate', () => {
map.current?.easeTo({
center: [einsatzortCenter.lng, einsatzortCenter.lat],
zoom: project?.mapZoom || 17,
duration: 600,
})
})
}
})
map.current.on('load', () => { map.current.on('load', () => {
const m = map.current const m = map.current