diff --git a/package.json b/package.json index eb12f26..e5f0916 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lageplan", - "version": "1.4.13", + "version": "1.4.14", "description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation", "private": true, "scripts": { diff --git a/src/app/app/page.tsx b/src/app/app/page.tsx index b186624..5dfe9a7 100644 --- a/src/app/app/page.tsx +++ b/src/app/app/page.tsx @@ -1021,6 +1021,8 @@ export default function AppPage() { undoDrawPointRef={undoDrawPointRef} pendingSymbol={pendingSymbol ? { iconId: pendingSymbol.id, imageUrl: pendingSymbol.imageUrl } : null} onSymbolPlaced={() => setPendingSymbol(null)} + isFullscreen={isFullscreen} + onToggleFullscreen={toggleFullscreen} /> diff --git a/src/components/dialogs/project-dialog.tsx b/src/components/dialogs/project-dialog.tsx index 820ff83..0bed4bf 100644 --- a/src/components/dialogs/project-dialog.tsx +++ b/src/components/dialogs/project-dialog.tsx @@ -33,6 +33,23 @@ interface NominatimResult { } } +// Baut aus einem Nominatim-Ergebnis eine kurze, lesbare Adresse (Strasse Nr, PLZ Ort) +function buildDisplayName(result: NominatimResult): string { + const addr = result.address + if (!addr) return result.display_name + const parts: string[] = [] + if (addr.road) { + parts.push(addr.road + (addr.house_number ? ' ' + addr.house_number : '')) + } + const city = addr.city || addr.town || addr.village || addr.municipality + if (addr.postcode && city) { + parts.push(`${addr.postcode} ${city}`) + } else if (city) { + parts.push(city) + } + return parts.length > 0 ? parts.join(', ') : result.display_name +} + interface ProjectDialogProps { open: boolean onOpenChange: (open: boolean) => void @@ -65,6 +82,9 @@ export function ProjectDialog({ const [isSearching, setIsSearching] = useState(false) const [showSuggestions, setShowSuggestions] = useState(false) const [selectedCoords, setSelectedCoords] = useState<{ lat: number; lng: number } | null>(null) + const [isLocating, setIsLocating] = useState(false) + const [locateError, setLocateError] = useState('') + const autoLocatedRef = useRef(false) const debounceRef = useRef(null) const suggestionsRef = useRef(null) @@ -103,28 +123,64 @@ export function ProjectDialog({ } const handleSelectSuggestion = (result: NominatimResult) => { - // Build a clean display name - const addr = result.address - let displayName = result.display_name - if (addr) { - const parts: string[] = [] - if (addr.road) { - parts.push(addr.road + (addr.house_number ? ' ' + addr.house_number : '')) - } - const city = addr.city || addr.town || addr.village || addr.municipality - if (addr.postcode && city) { - parts.push(`${addr.postcode} ${city}`) - } else if (city) { - parts.push(city) - } - if (parts.length > 0) displayName = parts.join(', ') - } - setLocation(displayName) + setLocation(buildDisplayName(result)) setSelectedCoords({ lat: parseFloat(result.lat), lng: parseFloat(result.lon) }) setSuggestions([]) setShowSuggestions(false) } + // Aktuellen Standort ermitteln und als Einsatzort vorschlagen (Reverse-Geocoding) + const applyCurrentLocation = useCallback(() => { + if (typeof navigator === 'undefined' || !navigator.geolocation) { + setLocateError('Standort auf diesem Gerät nicht verfügbar') + return + } + setIsLocating(true) + setLocateError('') + navigator.geolocation.getCurrentPosition( + async (pos) => { + const { latitude, longitude } = pos.coords + setSelectedCoords({ lat: latitude, lng: longitude }) + setShowSuggestions(false) + try { + const res = await fetch( + `https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json&addressdetails=1` + ) + if (res.ok) { + const data: NominatimResult = await res.json() + setLocation(buildDisplayName(data)) + } else { + setLocation(`${latitude.toFixed(5)}, ${longitude.toFixed(5)}`) + } + } catch { + setLocation(`${latitude.toFixed(5)}, ${longitude.toFixed(5)}`) + } finally { + setIsLocating(false) + } + }, + (err) => { + setIsLocating(false) + setLocateError( + err.code === err.PERMISSION_DENIED + ? 'Standortzugriff verweigert' + : 'Standort konnte nicht ermittelt werden' + ) + }, + { enableHighAccuracy: true, timeout: 8000, maximumAge: 60000 } + ) + }, []) + + // Beim Öffnen eines neuen Einsatzes den Standort automatisch als Ort vorschlagen + useEffect(() => { + if (open && mode === 'EINSATZ' && !autoLocatedRef.current && !location && !selectedCoords) { + autoLocatedRef.current = true + applyCurrentLocation() + } + if (!open) { + autoLocatedRef.current = false + } + }, [open, mode, location, selectedCoords, applyCurrentLocation]) + // Close suggestions on click outside useEffect(() => { const handleClickOutside = (e: MouseEvent) => { @@ -186,6 +242,8 @@ export function ProjectDialog({ setJournalfuehrer('') setSelectedCoords(null) setSuggestions([]) + setLocateError('') + setIsLocating(false) } catch (error) { toast({ title: 'Fehler', @@ -207,6 +265,8 @@ export function ProjectDialog({ setJournalfuehrer('') setSelectedCoords(null) setSuggestions([]) + setLocateError('') + setIsLocating(false) onOpenChange(false) } } @@ -269,14 +329,26 @@ export function ProjectDialog({
- +
+ + +
)}
-

- Adresse suchen — die Karte springt automatisch zum Einsatzort -

+ {locateError ? ( +

{locateError} — Adresse bitte manuell eingeben.

+ ) : ( +

+ Standort wird beim Öffnen vorgeschlagen — oder Adresse suchen. Die Karte springt automatisch zum Einsatzort. +

+ )}
diff --git a/src/components/map/map-view.tsx b/src/components/map/map-view.tsx index f5bd63d..a97234e 100644 --- a/src/components/map/map-view.tsx +++ b/src/components/map/map-view.tsx @@ -88,6 +88,9 @@ interface MapViewProps { /** Tap-to-place: wenn gesetzt, platziert der nächste Karten-Tipp dieses Symbol (Mobil-freundlich). */ pendingSymbol?: { iconId: string; imageUrl?: string } | null onSymbolPlaced?: () => void + /** Vollbild-Umschaltung (mobil als schwebender Button über der Karte) */ + isFullscreen?: boolean + onToggleFullscreen?: () => void } export function MapView({ @@ -105,6 +108,8 @@ export function MapView({ undoDrawPointRef, pendingSymbol, onSymbolPlaced, + isFullscreen, + onToggleFullscreen, }: MapViewProps) { const mapContainer = useRef(null) const map = useRef(null) @@ -2140,6 +2145,21 @@ export function MapView({ className="w-full h-full" /> + {/* Vollbild-Umschalter — mobil als schwebender Button (versteckt die Browser-Leiste) */} + {onToggleFullscreen && ( + + )} + {/* Moveable controls for selected symbol */} {isSymbolSelected && selectedSymbolRef.current && canEdit && ( <>