'use client' import { useState, useEffect } from 'react' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { ScrollArea } from '@/components/ui/scroll-area' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Flame, GraduationCap, Save, FolderOpen, Download, Sun, Moon, Maximize, Minimize, ClipboardList, Settings, LogOut, User, MoreVertical, Trash2, AlertTriangle, List, Eraser, ImagePlus, Key, Shield, MapPin, HelpCircle, Lock, Unlock, CheckCircle2, Plus, ChevronDown, Loader2, Share2, Wind, Satellite, } from 'lucide-react' import { HoseSettingsDialog } from '@/components/dialogs/hose-settings-dialog' import { ShareDialog } from '@/components/dialogs/share-dialog' import { degToCompass } from '@/components/map/map-compass' import type { Project, DrawFeature, ProjectMode } from '@/types' import { formatDateTime } from '@/lib/utils' import { Logo } from '@/components/ui/logo' // 8-Richtungs-Auswahl (Grad, meteorologisch „Wind aus") const WIND_OPTIONS: { label: string; deg: number }[] = [ { label: 'N', deg: 0 }, { label: 'NO', deg: 45 }, { label: 'O', deg: 90 }, { label: 'SO', deg: 135 }, { label: 'S', deg: 180 }, { label: 'SW', deg: 225 }, { label: 'W', deg: 270 }, { label: 'NW', deg: 315 }, ] interface TopbarProps { project: Project | null onNewProject: (mode?: ProjectMode) => void onEndProject?: () => void onSaveProject: () => void onLoadProject: (project: Project, features: DrawFeature[]) => void onDeleteProject?: (projectId: string) => void onExport: (format: 'png' | 'pdf') => void onClearAll?: () => void onPlanUpload?: () => void isSaving: boolean isDarkMode: boolean onToggleTheme: () => void isFullscreen: boolean onToggleFullscreen: () => void auditLog: { time: string; action: string }[] isAuditOpen: boolean onToggleAudit: () => void userName?: string userRole?: string onLogout?: () => void onStartTour?: () => void presentationLocked?: boolean onTogglePresentationLock?: () => void /** Windrichtung wird live aktualisiert (für die Live-Anzeige oben) */ windLive?: boolean /** Windrichtung setzen/entfernen (Grad, meteorologisch „Wind aus"; null = entfernen) */ onWindChange?: (dir: number | null) => void /** Live-Wind (Open-Meteo) vom Standort holen */ onFetchLiveWind?: () => Promise | void /** true, wenn der aktuelle Nutzer den Bearbeitungs-Lock hält */ isEditingByMe?: boolean /** Karte speichern & für andere freigeben (Lock lösen) */ onStopEditing?: () => void /** Lock-Aktion läuft gerade */ editingLoading?: boolean } export function Topbar({ project, onNewProject, onEndProject, onSaveProject, onLoadProject, onDeleteProject, onExport, onClearAll, onPlanUpload, isSaving, isDarkMode, onToggleTheme, isFullscreen, onToggleFullscreen, auditLog, isAuditOpen, onToggleAudit, userName, userRole, onLogout, onStartTour, presentationLocked, onTogglePresentationLock, windLive, onWindChange, onFetchLiveWind, isEditingByMe, onStopEditing, editingLoading, }: TopbarProps) { const [isLoadDialogOpen, setIsLoadDialogOpen] = useState(false) const [isHoseSettingsOpen, setIsHoseSettingsOpen] = useState(false) const [isShareOpen, setIsShareOpen] = useState(false) const [showPasswordDialog, setShowPasswordDialog] = useState(false) const [showDeleteAccountDialog, setShowDeleteAccountDialog] = useState(false) const [deleteAccountPw, setDeleteAccountPw] = useState('') const [deleteAccountLoading, setDeleteAccountLoading] = useState(false) const [deleteAccountError, setDeleteAccountError] = useState('') const [pwOld, setPwOld] = useState('') const [pwNew, setPwNew] = useState('') const [pwConfirm, setPwConfirm] = useState('') const [pwLoading, setPwLoading] = useState(false) const [pwStatus, setPwStatus] = useState<'success' | 'error' | null>(null) const [pwError, setPwError] = useState('') const [projects, setProjects] = useState([]) const [isLoadingProjects, setIsLoadingProjects] = useState(false) const [deleteConfirmId, setDeleteConfirmId] = useState(null) const [isDeleting, setIsDeleting] = useState(false) const [windLoading, setWindLoading] = useState(false) // Live clock const [now, setNow] = useState(new Date()) useEffect(() => { const timer = setInterval(() => setNow(new Date()), 1000) return () => clearInterval(timer) }, []) const handleOpenLoadDialog = async () => { setIsLoadDialogOpen(true) setIsLoadingProjects(true) try { const res = await fetch('/api/projects') if (res.ok) { const data = await res.json() setProjects(data.projects) } } catch (error) { console.error('Error loading projects:', error) } finally { setIsLoadingProjects(false) } } const handleLoadProject = async (projectId: string) => { try { const res = await fetch(`/api/projects/${projectId}`) if (res.ok) { const data = await res.json() onLoadProject(data.project, data.project.features || []) setIsLoadDialogOpen(false) } } catch (error) { console.error('Error loading project:', error) } } const handleExport = (format: 'png' | 'pdf') => { onExport(format) } return (
{/* Zone 1 — Identität */}
LAGEPLAN v{process.env.APP_VERSION}
{/* Zone 2 — Aktiver Einsatz (Blickfang) */}
{project ? (
{(project as any).einsatzNr && ( {(project as any).einsatzNr} )} {project.title} {project.location && ( · {project.location} )}
) : ( Kein Einsatz aktiv )}
{/* Zone 3 — Uhr + Aktionen */}
{/* Wind — klare, einzige Wind-Anzeige. Klick öffnet den Einsteller (Live vom Standort oder Richtung wählen). Der Pfeil zeigt die Ausbreitungsrichtung, N oben. */} {project && onWindChange && ( {typeof project.windDirection === 'number' ? ( ) : ( )}
Windrichtung (Wind aus)
{onFetchLiveWind && ( )}
oder Richtung wählen:
{WIND_OPTIONS.map((w) => ( ))}
{typeof project.windDirection === 'number' && ( )}
)} {/* Uhr — Ops-Rooms leben nach der Uhr */} {now.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' })} {/* Speichern */} {/* Schnellzugriff: Vollbild + Präsentationsmodus (Desktop; mobil im ⋮-Menü) */} {onTogglePresentationLock && ( )} {/* Freigeben — nur sichtbar, solange ich den Bearbeitungs-Lock halte. Speichert und gibt die Karte für andere frei. Grün = ich bin am Zeichnen. */} {isEditingByMe && onStopEditing && ( )} {/* Neu — Split-Button für Einsatz/Übung (ein Akzent statt zwei farbiger Buttons) */} onNewProject('EINSATZ')} className="py-2.5"> Neuer Einsatz onNewProject('UEBUNG')} className="py-2.5"> Neue Übung {/* Werkzeuge (Desktop) — sammelt alle Neben-Aktionen in einem Menü */} Einsätze verwalten {project && ( setIsShareOpen(true)}> Lage teilen (Nur-Ansicht) )} handleExport('png')}> Als PNG exportieren handleExport('pdf')}> Als PDF exportieren {onClearAll && ( Zeichnung leeren )} {onTogglePresentationLock && ( {presentationLocked ? : } {presentationLocked ? 'Präsentation: gesperrt' : 'Präsentationsmodus'} )} {isFullscreen ? : } {isFullscreen ? 'Vollbild verlassen' : 'Vollbild'} {isDarkMode ? : } {isDarkMode ? 'Tagmodus' : 'Nachtmodus'} setIsHoseSettingsOpen(true)}> Schlaucheinstellungen {/* Einsatz beenden (Desktop) */} {project && onEndProject && ( )} {/* Benutzer (Desktop) */} {userName && onLogout && (

{userName}

{userRole &&

{userRole}

}
setShowPasswordDialog(true)}> Kennwort ändern window.location.href = '/settings'}> Sicherheit & 2FA window.location.href = '/konto/datenschutz'}> Konto & Datenschutz {(userRole === 'SERVER_ADMIN' || userRole === 'TENANT_ADMIN') && ( window.location.href = '/admin'}> Administration )} {onStartTour && ( Tour starten )} { setShowDeleteAccountDialog(true); setDeleteAccountPw(''); setDeleteAccountError('') }} className="text-destructive focus:text-destructive" > Konto löschen Abmelden
)} {/* Mobile: EIN Menü mit allen Aktionen (ersetzt die früheren zwei getrennten Menüs) */} {userName && ( <> {userName}{userRole ? ` · ${userRole}` : ''} )} {/* Einsatz / Übung (Erstellen liegt im „Neu"-Button) */} {project && onEndProject && ( {project.mode === 'UEBUNG' ? 'Übung beenden' : 'Einsatz beenden'} )} Einsätze verwalten {project && ( setIsShareOpen(true)}> Lage teilen (Nur-Ansicht) )} {/* Karte / Export */} handleExport('png')}> Als PNG exportieren handleExport('pdf')}> Als PDF exportieren {onClearAll && ( Zeichnung leeren )} {onTogglePresentationLock && ( {presentationLocked ? : } {presentationLocked ? 'Präsentation: gesperrt' : 'Präsentationsmodus'} )} {/* Ansicht */} {isFullscreen ? : } {isFullscreen ? 'Vollbild verlassen' : 'Vollbild'} {isDarkMode ? : } {isDarkMode ? 'Tagmodus' : 'Nachtmodus'} {/* System / Konto */} {(userRole === 'SERVER_ADMIN' || userRole === 'TENANT_ADMIN') && ( window.location.href = '/admin'}> Administration )} setIsHoseSettingsOpen(true)}> Schlaucheinstellungen {onStartTour && ( Tour starten )} setShowPasswordDialog(true)}> Kennwort ändern window.location.href = '/settings'}> Sicherheit & 2FA window.location.href = '/konto/datenschutz'}> Konto & Datenschutz {onLogout && ( Abmelden )}
{/* Audit Trail Panel */} {isAuditOpen && (
Audit Trail
{auditLog.length === 0 ? (

Noch keine Aktionen

) : ( auditLog.map((entry, i) => (
{entry.time} {entry.action}
)) )}
)} {/* Project Management Dialog */} Einsätze verwalten {isLoadingProjects ? (
Laden...
) : projects.length === 0 ? (
Keine Einsätze vorhanden
) : (
{projects.map((p) => (
))}
)}
{/* Delete Confirmation Dialog */} setDeleteConfirmId(null)}> Einsatz löschen?

Dieser Einsatz wird unwiderruflich gelöscht, inklusive aller Zeichnungen, Journal-Einträge und Daten.

{/* Password Change Dialog */} { setShowPasswordDialog(open) if (!open) { setPwOld(''); setPwNew(''); setPwConfirm(''); setPwStatus(null); setPwError('') } }}> Kennwort ändern
setPwOld(e.target.value)} className="w-full border rounded-md px-3 py-2 text-sm mt-1" autoComplete="current-password" />
setPwNew(e.target.value)} className="w-full border rounded-md px-3 py-2 text-sm mt-1" autoComplete="new-password" />
setPwConfirm(e.target.value)} className="w-full border rounded-md px-3 py-2 text-sm mt-1" autoComplete="new-password" />
{pwStatus === 'error' &&

{pwError}

} {pwStatus === 'success' &&

Kennwort erfolgreich geändert!

}
{/* Delete Account Dialog */} Konto löschen

Ihr Konto wird unwiderruflich gelöscht. Ihre Projekte und Daten bleiben der Organisation erhalten, aber Ihr persönlicher Zugang wird entfernt.

{userRole === 'TENANT_ADMIN' && (
Hinweis: Als einziger Administrator müssen Sie zuerst die Organisation unter Einstellungen löschen oder die Admin-Rolle übertragen.
)}
{ setDeleteAccountPw(e.target.value); setDeleteAccountError('') }} placeholder="Ihr Passwort" className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm" autoComplete="current-password" />
{deleteAccountError && (

{deleteAccountError}

)}
) }