Files
Lageplan/src/components/layout/topbar.tsx
Pepe Ziberi ae78b2d878 fix(nav): Sicherheit/2FA & Konto-Datenschutz im Benutzer-Menü auffindbar (v1.8.3)
2FA lag unter /settings, war aber über kein Menü erreichbar. Neu im Benutzer-Menü
(Desktop-Dropdown + Mobil-Menü): "Sicherheit & 2FA" (→ /settings) und
"Konto & Datenschutz" (→ /konto/datenschutz).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:15:40 +02:00

815 lines
34 KiB
TypeScript

'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,
} 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'
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
/** 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,
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<Project[]>([])
const [isLoadingProjects, setIsLoadingProjects] = useState(false)
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
const [isDeleting, setIsDeleting] = 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 (
<header className="h-14 md:h-16 border-b border-border bg-card flex items-center gap-2 md:gap-3 px-2 md:px-4 shrink-0 print:hidden relative">
{/* Zone 1 — Identität */}
<div className="flex items-center gap-2 shrink-0">
<Logo size={32} />
<div className="hidden sm:flex flex-col leading-none">
<span className="font-bold text-sm tracking-wide">LAGEPLAN</span>
<span className="text-[10px] text-muted-foreground font-mono mt-0.5">v{process.env.APP_VERSION}</span>
</div>
</div>
<div className="h-8 w-px bg-border hidden md:block shrink-0" />
{/* Zone 2 — Aktiver Einsatz (Blickfang) */}
<div className="flex-1 min-w-0 flex items-center">
{project ? (
<div className="flex items-center gap-2 min-w-0 rounded-lg border border-border bg-muted/40 pl-2.5 pr-3 py-1.5">
<span
className={`w-2.5 h-2.5 rounded-full shrink-0 ${project.mode === 'UEBUNG' ? 'bg-blue-500' : 'bg-red-500 animate-pulse'}`}
title={project.mode === 'UEBUNG' ? 'Übung' : 'Einsatz'}
/>
{(project as any).einsatzNr && (
<span className="font-mono text-xs font-bold shrink-0">{(project as any).einsatzNr}</span>
)}
<span className="font-semibold text-sm truncate">{project.title}</span>
{project.location && (
<span className="hidden lg:inline text-sm text-muted-foreground truncate">· {project.location}</span>
)}
</div>
) : (
<span className="text-sm text-muted-foreground hidden sm:inline">Kein Einsatz aktiv</span>
)}
</div>
{/* Zone 3 — Uhr + Aktionen */}
<div className="flex items-center gap-1 md:gap-1.5 shrink-0">
{/* Wind — taktische Live-Anzeige (Pfeil zeigt Ausbreitungsrichtung, N oben) */}
{typeof project?.windDirection === 'number' && (
<div
className="hidden sm:flex items-center gap-1.5 rounded-md bg-blue-600 text-white pl-1.5 pr-2 py-1 shrink-0"
title={`Wind aus ${degToCompass(project.windDirection)} (${project.windDirection}°) — treibt nach ${degToCompass(project.windDirection + 180)}${windLive ? ' · live' : ''}`}
>
<svg viewBox="0 0 24 24" className="w-4 h-4 shrink-0" style={{ transform: `rotate(${project.windDirection + 180}deg)` }} aria-hidden>
<path d="M12 3 L18 16 L12 12.5 L6 16 Z" fill="currentColor" />
</svg>
<span className="text-xs font-bold leading-none whitespace-nowrap">
<span className="hidden lg:inline opacity-80 font-medium">Wind </span>aus {degToCompass(project.windDirection)}
</span>
{windLive && <span className="w-1.5 h-1.5 rounded-full bg-green-300 animate-pulse shrink-0" title="Live" />}
</div>
)}
{/* Uhr — Ops-Rooms leben nach der Uhr */}
<span className="hidden sm:inline text-base md:text-lg font-bold tabular-nums mr-0.5 md:mr-1.5">
{now.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' })}
</span>
{/* Speichern */}
<Button
data-tour="save"
variant="outline"
size="icon"
className="h-9 w-9 md:h-10 md:w-10 shrink-0"
onClick={onSaveProject}
disabled={!project || isSaving}
title="Speichern (Ctrl+S)"
>
{isSaving ? <Loader2 className="w-5 h-5 animate-spin" /> : <Save className="w-5 h-5" />}
</Button>
{/* 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 && (
<Button
variant="outline"
className="h-9 md:h-10 px-2 md:px-2.5 shrink-0 border-green-500 text-green-700 hover:bg-green-50 dark:text-green-400 dark:hover:bg-green-950/40"
onClick={onStopEditing}
disabled={editingLoading}
title="Karte speichern & für andere freigeben"
>
{editingLoading ? <Loader2 className="w-5 h-5 animate-spin lg:mr-1" /> : <Unlock className="w-5 h-5 lg:mr-1" />}
<span className="hidden lg:inline">Freigeben</span>
</Button>
)}
{/* Neu — Split-Button für Einsatz/Übung (ein Akzent statt zwei farbiger Buttons) */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button data-tour="new-project" className="h-9 md:h-10 px-2 md:px-2.5 shrink-0" title="Neu erstellen">
<Plus className="w-5 h-5 lg:mr-1" />
<span className="hidden lg:inline">Neu</span>
<ChevronDown className="w-3.5 h-3.5 ml-0.5 opacity-70" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={() => onNewProject('EINSATZ')} className="py-2.5">
<Flame className="w-4 h-4 mr-2 text-red-600" />
Neuer Einsatz
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onNewProject('UEBUNG')} className="py-2.5">
<GraduationCap className="w-4 h-4 mr-2 text-blue-600" />
Neue Übung
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Werkzeuge (Desktop) — sammelt alle Neben-Aktionen in einem Menü */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="hidden md:flex h-10 w-10 shrink-0" title="Werkzeuge">
<Settings className="w-5 h-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem onClick={handleOpenLoadDialog}>
<List className="w-4 h-4 mr-2" />
Einsätze verwalten
</DropdownMenuItem>
{project && (
<DropdownMenuItem onClick={() => setIsShareOpen(true)}>
<Share2 className="w-4 h-4 mr-2" />
Lage teilen (Nur-Ansicht)
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleExport('png')}>
<Download className="w-4 h-4 mr-2" />
Als PNG exportieren
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport('pdf')}>
<Download className="w-4 h-4 mr-2" />
Als PDF exportieren
</DropdownMenuItem>
{onClearAll && (
<DropdownMenuItem onClick={onClearAll} className="text-destructive focus:text-destructive">
<Eraser className="w-4 h-4 mr-2" />
Zeichnung leeren
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
{onTogglePresentationLock && (
<DropdownMenuItem onClick={onTogglePresentationLock}>
{presentationLocked ? <Lock className="w-4 h-4 mr-2" /> : <Unlock className="w-4 h-4 mr-2" />}
{presentationLocked ? 'Präsentation: gesperrt' : 'Präsentationsmodus'}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={onToggleFullscreen}>
{isFullscreen ? <Minimize className="w-4 h-4 mr-2" /> : <Maximize className="w-4 h-4 mr-2" />}
{isFullscreen ? 'Vollbild verlassen' : 'Vollbild'}
</DropdownMenuItem>
<DropdownMenuItem onClick={onToggleTheme}>
{isDarkMode ? <Sun className="w-4 h-4 mr-2" /> : <Moon className="w-4 h-4 mr-2" />}
{isDarkMode ? 'Tagmodus' : 'Nachtmodus'}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsHoseSettingsOpen(true)}>
<Settings className="w-4 h-4 mr-2" />
Schlaucheinstellungen
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Einsatz beenden (Desktop) */}
{project && onEndProject && (
<Button
variant="outline"
className="hidden md:flex h-10 px-2 lg:px-3 shrink-0 border-red-300 text-red-600 hover:bg-red-50 hover:text-red-700 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-950/40"
onClick={onEndProject}
title={project.mode === 'UEBUNG' ? 'Übung speichern und schliessen' : 'Einsatz speichern und schliessen'}
>
<CheckCircle2 className="w-4 h-4 xl:mr-1" />
<span className="hidden xl:inline">Beenden</span>
</Button>
)}
{/* Benutzer (Desktop) */}
{userName && onLogout && (
<div className="hidden md:flex items-center ml-0.5 pl-1.5 border-l border-border">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-10 w-10 text-muted-foreground hover:text-foreground" title={userName}>
<User className="w-5 h-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<div className="px-3 py-2 border-b">
<p className="text-sm font-medium">{userName}</p>
{userRole && <p className="text-xs text-muted-foreground">{userRole}</p>}
</div>
<DropdownMenuItem onClick={() => setShowPasswordDialog(true)}>
<Key className="w-4 h-4 mr-2" />
Kennwort ändern
</DropdownMenuItem>
<DropdownMenuItem onClick={() => window.location.href = '/settings'}>
<Lock className="w-4 h-4 mr-2" />
Sicherheit &amp; 2FA
</DropdownMenuItem>
<DropdownMenuItem onClick={() => window.location.href = '/konto/datenschutz'}>
<Shield className="w-4 h-4 mr-2" />
Konto &amp; Datenschutz
</DropdownMenuItem>
{(userRole === 'SERVER_ADMIN' || userRole === 'TENANT_ADMIN') && (
<DropdownMenuItem onClick={() => window.location.href = '/admin'}>
<Shield className="w-4 h-4 mr-2" />
Administration
</DropdownMenuItem>
)}
{onStartTour && (
<DropdownMenuItem onClick={onStartTour}>
<HelpCircle className="w-4 h-4 mr-2" />
Tour starten
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => { setShowDeleteAccountDialog(true); setDeleteAccountPw(''); setDeleteAccountError('') }}
className="text-destructive focus:text-destructive"
>
<Trash2 className="w-4 h-4 mr-2" />
Konto löschen
</DropdownMenuItem>
<DropdownMenuItem onClick={onLogout} className="text-destructive focus:text-destructive">
<LogOut className="w-4 h-4 mr-2" />
Abmelden
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{/* Mobile: EIN Menü mit allen Aktionen (ersetzt die früheren zwei getrennten Menüs) */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="md:hidden h-9 px-2" title="Menü">
<MoreVertical className="w-5 h-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 max-h-[80vh] overflow-y-auto">
{userName && (
<>
<DropdownMenuLabel className="text-xs text-muted-foreground font-normal">
{userName}{userRole ? ` · ${userRole}` : ''}
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
)}
{/* Einsatz / Übung (Erstellen liegt im „Neu"-Button) */}
{project && onEndProject && (
<DropdownMenuItem onClick={onEndProject} className="text-red-600 focus:text-red-700">
<CheckCircle2 className="w-4 h-4 mr-2" />
{project.mode === 'UEBUNG' ? 'Übung beenden' : 'Einsatz beenden'}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={handleOpenLoadDialog}>
<List className="w-4 h-4 mr-2" />
Einsätze verwalten
</DropdownMenuItem>
{project && (
<DropdownMenuItem onClick={() => setIsShareOpen(true)}>
<Share2 className="w-4 h-4 mr-2" />
Lage teilen (Nur-Ansicht)
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
{/* Karte / Export */}
<DropdownMenuItem onClick={() => handleExport('png')}>
<Download className="w-4 h-4 mr-2" />
Als PNG exportieren
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport('pdf')}>
<Download className="w-4 h-4 mr-2" />
Als PDF exportieren
</DropdownMenuItem>
{onClearAll && (
<DropdownMenuItem onClick={onClearAll} className="text-destructive focus:text-destructive">
<Eraser className="w-4 h-4 mr-2" />
Zeichnung leeren
</DropdownMenuItem>
)}
{onTogglePresentationLock && (
<DropdownMenuItem onClick={onTogglePresentationLock}>
{presentationLocked ? <Lock className="w-4 h-4 mr-2" /> : <Unlock className="w-4 h-4 mr-2" />}
{presentationLocked ? 'Präsentation: gesperrt' : 'Präsentationsmodus'}
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
{/* Ansicht */}
<DropdownMenuItem onClick={onToggleFullscreen}>
{isFullscreen ? <Minimize className="w-4 h-4 mr-2" /> : <Maximize className="w-4 h-4 mr-2" />}
{isFullscreen ? 'Vollbild verlassen' : 'Vollbild'}
</DropdownMenuItem>
<DropdownMenuItem onClick={onToggleTheme}>
{isDarkMode ? <Sun className="w-4 h-4 mr-2" /> : <Moon className="w-4 h-4 mr-2" />}
{isDarkMode ? 'Tagmodus' : 'Nachtmodus'}
</DropdownMenuItem>
<DropdownMenuSeparator />
{/* System / Konto */}
{(userRole === 'SERVER_ADMIN' || userRole === 'TENANT_ADMIN') && (
<DropdownMenuItem onClick={() => window.location.href = '/admin'}>
<Shield className="w-4 h-4 mr-2" />
Administration
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => setIsHoseSettingsOpen(true)}>
<Settings className="w-4 h-4 mr-2" />
Schlaucheinstellungen
</DropdownMenuItem>
{onStartTour && (
<DropdownMenuItem onClick={onStartTour}>
<HelpCircle className="w-4 h-4 mr-2" />
Tour starten
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => setShowPasswordDialog(true)}>
<Key className="w-4 h-4 mr-2" />
Kennwort ändern
</DropdownMenuItem>
<DropdownMenuItem onClick={() => window.location.href = '/settings'}>
<Lock className="w-4 h-4 mr-2" />
Sicherheit &amp; 2FA
</DropdownMenuItem>
<DropdownMenuItem onClick={() => window.location.href = '/konto/datenschutz'}>
<Shield className="w-4 h-4 mr-2" />
Konto &amp; Datenschutz
</DropdownMenuItem>
{onLogout && (
<DropdownMenuItem onClick={onLogout} className="text-destructive focus:text-destructive">
<LogOut className="w-4 h-4 mr-2" />
Abmelden
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Audit Trail Panel */}
{isAuditOpen && (
<div className="absolute right-0 top-16 w-96 max-h-[60vh] z-50 bg-card border border-border rounded-bl-lg shadow-xl overflow-hidden flex flex-col">
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-muted/50">
<span className="font-semibold text-sm flex items-center gap-2">
<ClipboardList className="w-4 h-4" /> Audit Trail
</span>
<button onClick={onToggleAudit} className="text-muted-foreground hover:text-foreground text-lg leading-none"></button>
</div>
<ScrollArea className="flex-1 max-h-[calc(60vh-48px)]">
<div className="p-2">
{auditLog.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">Noch keine Aktionen</p>
) : (
auditLog.map((entry, i) => (
<div key={i} className="flex gap-3 px-3 py-2 text-sm border-b border-border/50 last:border-0">
<span className="text-muted-foreground font-mono text-xs whitespace-nowrap pt-0.5">{entry.time}</span>
<span>{entry.action}</span>
</div>
))
)}
</div>
</ScrollArea>
</div>
)}
{/* Project Management Dialog */}
<Dialog open={isLoadDialogOpen} onOpenChange={setIsLoadDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<List className="w-5 h-5" />
Einsätze verwalten
</DialogTitle>
</DialogHeader>
<ScrollArea className="h-[450px] pr-4">
{isLoadingProjects ? (
<div className="flex items-center justify-center h-32">
<span className="text-muted-foreground">Laden...</span>
</div>
) : projects.length === 0 ? (
<div className="flex items-center justify-center h-32">
<span className="text-muted-foreground">Keine Einsätze vorhanden</span>
</div>
) : (
<div className="space-y-2">
{projects.map((p) => (
<div
key={p.id}
className={`p-4 border rounded-lg transition-colors ${
project?.id === p.id ? 'border-primary bg-primary/5' : 'hover:bg-accent'
}`}
>
<div className="flex items-start justify-between gap-3">
<button
onClick={() => handleLoadProject(p.id)}
className="flex-1 text-left"
>
<div className="flex items-center gap-2">
<h4 className="font-medium">{p.title}</h4>
{(p as any).mode === 'UEBUNG' ? (
<span className="text-xs bg-blue-100 text-blue-700 dark:bg-blue-950/50 dark:text-blue-300 px-1.5 py-0.5 rounded">Übung</span>
) : (
<span className="text-xs bg-red-100 text-red-700 dark:bg-red-950/50 dark:text-red-300 px-1.5 py-0.5 rounded">Einsatz</span>
)}
{project?.id === p.id && (
<span className="text-xs bg-primary text-primary-foreground px-1.5 py-0.5 rounded">Aktiv</span>
)}
</div>
{p.location && (
<p className="text-sm text-muted-foreground mt-1">
<MapPin className="w-3 h-3 inline mr-1" />{p.location}
</p>
)}
<p className="text-xs text-muted-foreground mt-1">
{(p as any).owner?.name && (
<><span className="font-medium text-foreground/70">{(p as any).owner.name}</span> · </>
)}
Erstellt: {formatDateTime(p.createdAt)} | Geändert: {formatDateTime(p.updatedAt)}
</p>
</button>
<div className="flex items-center gap-1 shrink-0">
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => handleLoadProject(p.id)}
title="Einsatz öffnen"
>
<FolderOpen className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8 text-destructive hover:bg-destructive hover:text-destructive-foreground"
onClick={() => setDeleteConfirmId(p.id)}
title="Einsatz löschen"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</div>
))}
</div>
)}
</ScrollArea>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={!!deleteConfirmId} onOpenChange={() => setDeleteConfirmId(null)}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-destructive">
<AlertTriangle className="w-5 h-5" />
Einsatz löschen?
</DialogTitle>
</DialogHeader>
<p className="text-sm text-muted-foreground">
Dieser Einsatz wird unwiderruflich gelöscht, inklusive aller Zeichnungen, Journal-Einträge und Daten.
</p>
<div className="flex justify-end gap-2 mt-2">
<Button variant="outline" onClick={() => setDeleteConfirmId(null)} disabled={isDeleting}>
Abbrechen
</Button>
<Button
variant="destructive"
disabled={isDeleting}
onClick={async () => {
if (!deleteConfirmId) return
setIsDeleting(true)
try {
const res = await fetch(`/api/projects/${deleteConfirmId}`, { method: 'DELETE' })
if (res.ok) {
setProjects(prev => prev.filter(p => p.id !== deleteConfirmId))
if (onDeleteProject) onDeleteProject(deleteConfirmId)
setDeleteConfirmId(null)
}
} catch (e) {
console.error('Delete failed:', e)
} finally {
setIsDeleting(false)
}
}}
>
{isDeleting ? 'Löschen...' : 'Endgültig löschen'}
</Button>
</div>
</DialogContent>
</Dialog>
<HoseSettingsDialog open={isHoseSettingsOpen} onOpenChange={setIsHoseSettingsOpen} />
<ShareDialog open={isShareOpen} onOpenChange={setIsShareOpen} projectId={project?.id ?? null} projectTitle={project?.title} />
{/* Password Change Dialog */}
<Dialog open={showPasswordDialog} onOpenChange={(open) => {
setShowPasswordDialog(open)
if (!open) { setPwOld(''); setPwNew(''); setPwConfirm(''); setPwStatus(null); setPwError('') }
}}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Key className="w-5 h-5" />
Kennwort ändern
</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<div>
<label className="text-xs font-medium text-muted-foreground">Aktuelles Kennwort</label>
<input type="password" value={pwOld} onChange={e => setPwOld(e.target.value)} className="w-full border rounded-md px-3 py-2 text-sm mt-1" autoComplete="current-password" />
</div>
<div>
<label className="text-xs font-medium text-muted-foreground">Neues Kennwort</label>
<input type="password" value={pwNew} onChange={e => setPwNew(e.target.value)} className="w-full border rounded-md px-3 py-2 text-sm mt-1" autoComplete="new-password" />
</div>
<div>
<label className="text-xs font-medium text-muted-foreground">Kennwort bestätigen</label>
<input type="password" value={pwConfirm} onChange={e => setPwConfirm(e.target.value)} className="w-full border rounded-md px-3 py-2 text-sm mt-1" autoComplete="new-password" />
</div>
{pwStatus === 'error' && <p className="text-sm text-destructive">{pwError}</p>}
{pwStatus === 'success' && <p className="text-sm text-green-600">Kennwort erfolgreich geändert!</p>}
</div>
<div className="flex justify-end gap-2 mt-2">
<Button variant="outline" size="sm" onClick={() => setShowPasswordDialog(false)}>Abbrechen</Button>
<Button
size="sm"
disabled={pwLoading || !pwOld || !pwNew || pwNew !== pwConfirm || pwNew.length < 6}
onClick={async () => {
setPwLoading(true)
setPwStatus(null)
setPwError('')
try {
const res = await fetch('/api/auth/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ currentPassword: pwOld, newPassword: pwNew }),
})
const data = await res.json()
if (res.ok) {
setPwStatus('success')
setPwOld(''); setPwNew(''); setPwConfirm('')
} else {
setPwStatus('error')
setPwError(data.error || 'Fehler beim Ändern')
}
} catch { setPwStatus('error'); setPwError('Verbindungsfehler') }
finally { setPwLoading(false) }
}}
>
{pwLoading ? 'Speichern...' : 'Kennwort ändern'}
</Button>
</div>
</DialogContent>
</Dialog>
{/* Delete Account Dialog */}
<Dialog open={showDeleteAccountDialog} onOpenChange={setShowDeleteAccountDialog}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-destructive">
<AlertTriangle className="w-5 h-5" />
Konto löschen
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Ihr Konto wird unwiderruflich gelöscht. Ihre Projekte und Daten bleiben der Organisation erhalten,
aber Ihr persönlicher Zugang wird entfernt.
</p>
{userRole === 'TENANT_ADMIN' && (
<div className="bg-amber-50 dark:bg-amber-950/30 rounded-lg p-3 text-xs text-amber-800 dark:text-amber-300 border border-amber-200 dark:border-amber-800">
<strong>Hinweis:</strong> Als einziger Administrator müssen Sie zuerst die Organisation unter Einstellungen löschen oder die Admin-Rolle übertragen.
</div>
)}
<div className="space-y-1.5">
<label className="text-sm font-medium">Passwort zur Bestätigung</label>
<input
type="password"
value={deleteAccountPw}
onChange={(e) => { 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"
/>
</div>
{deleteAccountError && (
<p className="text-sm text-destructive">{deleteAccountError}</p>
)}
<div className="flex gap-2 justify-end">
<Button
variant="outline"
size="sm"
onClick={() => setShowDeleteAccountDialog(false)}
disabled={deleteAccountLoading}
>
Abbrechen
</Button>
<Button
variant="destructive"
size="sm"
disabled={deleteAccountLoading || !deleteAccountPw}
onClick={async () => {
setDeleteAccountLoading(true)
setDeleteAccountError('')
try {
const res = await fetch('/api/auth/delete-account', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: deleteAccountPw }),
})
const data = await res.json()
if (res.ok) {
window.location.href = '/'
} else {
setDeleteAccountError(data.error || 'Löschung fehlgeschlagen')
}
} catch {
setDeleteAccountError('Verbindungsfehler')
} finally {
setDeleteAccountLoading(false)
}
}}
>
{deleteAccountLoading ? 'Wird gelöscht...' : 'Konto endgültig löschen'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</header>
)
}