631 lines
26 KiB
TypeScript
631 lines
26 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,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import {
|
|
Plus,
|
|
Save,
|
|
FolderOpen,
|
|
Download,
|
|
Clock,
|
|
Sun,
|
|
Moon,
|
|
Maximize,
|
|
Minimize,
|
|
ClipboardList,
|
|
Settings,
|
|
LogOut,
|
|
User,
|
|
MoreVertical,
|
|
Trash2,
|
|
AlertTriangle,
|
|
List,
|
|
Eraser,
|
|
ImagePlus,
|
|
Key,
|
|
Shield,
|
|
Building2,
|
|
MapPin,
|
|
} from 'lucide-react'
|
|
import { HoseSettingsDialog } from '@/components/dialogs/hose-settings-dialog'
|
|
import type { Project, DrawFeature } from '@/app/app/page'
|
|
import { formatDateTime } from '@/lib/utils'
|
|
import { Logo } from '@/components/ui/logo'
|
|
|
|
interface TopbarProps {
|
|
project: Project | null
|
|
onNewProject: () => 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
|
|
}
|
|
|
|
export function Topbar({
|
|
project,
|
|
onNewProject,
|
|
onSaveProject,
|
|
onLoadProject,
|
|
onDeleteProject,
|
|
onExport,
|
|
onClearAll,
|
|
onPlanUpload,
|
|
isSaving,
|
|
isDarkMode,
|
|
onToggleTheme,
|
|
isFullscreen,
|
|
onToggleFullscreen,
|
|
auditLog,
|
|
isAuditOpen,
|
|
onToggleAudit,
|
|
userName,
|
|
userRole,
|
|
onLogout,
|
|
}: TopbarProps) {
|
|
const [isLoadDialogOpen, setIsLoadDialogOpen] = useState(false)
|
|
const [isHoseSettingsOpen, setIsHoseSettingsOpen] = 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 justify-between px-2 md:px-4 shrink-0 overflow-x-auto print:hidden">
|
|
<div className="flex items-center gap-1.5 md:gap-4 shrink-0">
|
|
<div className="flex items-center gap-1.5 md:gap-2">
|
|
<Logo size={32} />
|
|
<span className="font-semibold text-lg hidden md:inline">Lageplan</span>
|
|
<span className="text-[10px] text-muted-foreground hidden md:inline">v{process.env.APP_VERSION}</span>
|
|
</div>
|
|
|
|
<div className="h-6 w-px bg-border hidden md:block" />
|
|
|
|
<div className="flex items-center gap-1 md:gap-2">
|
|
<Button
|
|
variant="outline"
|
|
className="h-9 md:h-10 px-2 md:px-4 text-sm"
|
|
onClick={onSaveProject}
|
|
disabled={!project || isSaving}
|
|
title="Speichern"
|
|
>
|
|
<Save className="w-5 h-5 md:mr-1.5" />
|
|
<span className="hidden lg:inline">{isSaving ? 'Speichern...' : 'Speichern'}</span>
|
|
</Button>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" className="h-9 md:h-10 px-2 md:px-3 text-sm" title="Menü">
|
|
<MoreVertical className="w-5 h-5 md:mr-1" />
|
|
<span className="hidden lg:inline">Menü</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-52">
|
|
<DropdownMenuItem onClick={onNewProject} className="py-2.5 px-3">
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Neuer Einsatz
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={handleOpenLoadDialog} className="py-2.5 px-3">
|
|
<List className="w-4 h-4 mr-2" />
|
|
Einsätze verwalten
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleExport('png')} className="py-2.5 px-3">
|
|
<Download className="w-4 h-4 mr-2" />
|
|
Als PNG exportieren
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleExport('pdf')} className="py-2.5 px-3">
|
|
<Download className="w-4 h-4 mr-2" />
|
|
Als PDF exportieren
|
|
</DropdownMenuItem>
|
|
{onClearAll && (
|
|
<DropdownMenuItem onClick={onClearAll} className="py-2.5 px-3 text-destructive focus:text-destructive">
|
|
<Eraser className="w-4 h-4 mr-2" />
|
|
Zeichnung leeren
|
|
</DropdownMenuItem>
|
|
)}
|
|
{(userRole === 'SERVER_ADMIN' || userRole === 'TENANT_ADMIN') && (
|
|
<DropdownMenuItem onClick={() => window.location.href = '/admin'} className="py-2.5 px-3">
|
|
<Settings className="w-4 h-4 mr-2" />
|
|
Einstellungen
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1 md:gap-2 shrink-0">
|
|
{/* Project info - desktop only */}
|
|
{project && (
|
|
<div className="hidden xl:flex items-center gap-2 text-sm border border-border rounded-lg px-3 py-1.5 bg-muted/30">
|
|
{(project as any).einsatzNr && (
|
|
<span className="font-mono text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded">{(project as any).einsatzNr}</span>
|
|
)}
|
|
<span className="font-semibold truncate max-w-[180px]">{project.title}</span>
|
|
{project.location && (
|
|
<><span className="text-muted-foreground">|</span><span className="text-muted-foreground truncate max-w-[150px]">{project.location}</span></>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Clock - desktop only */}
|
|
<div className="hidden lg:flex items-center gap-2 text-sm font-medium tabular-nums">
|
|
<Clock className="w-4 h-4 text-muted-foreground" />
|
|
<span className="text-primary font-bold">{now.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' })}</span>
|
|
</div>
|
|
|
|
{/* Desktop: individual buttons */}
|
|
<Button
|
|
variant={isDarkMode ? 'default' : 'outline'}
|
|
className="hidden md:flex h-9 px-2 gap-1.5 text-sm font-semibold"
|
|
onClick={onToggleTheme}
|
|
title={isDarkMode ? 'Tagmodus' : 'Nachtmodus'}
|
|
>
|
|
{isDarkMode ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
className="hidden md:flex h-9 px-2"
|
|
onClick={onToggleFullscreen}
|
|
title={isFullscreen ? 'Vollbild verlassen' : 'Vollbild'}
|
|
>
|
|
{isFullscreen ? <Minimize className="w-4 h-4" /> : <Maximize className="w-4 h-4" />}
|
|
</Button>
|
|
|
|
<Button
|
|
variant={isAuditOpen ? 'default' : 'outline'}
|
|
className="hidden md:flex h-9 px-2 relative"
|
|
onClick={onToggleAudit}
|
|
title="Audit Trail"
|
|
>
|
|
<ClipboardList className="w-4 h-4" />
|
|
{auditLog.length > 0 && (
|
|
<span className="absolute -top-1 -right-1 w-4 h-4 bg-primary text-primary-foreground text-[10px] rounded-full flex items-center justify-center font-bold">
|
|
{auditLog.length > 99 ? '99' : auditLog.length}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
|
|
{/* Desktop: User menu dropdown */}
|
|
{userName && onLogout && (
|
|
<div className="hidden md:flex items-center ml-1 pl-2 border-l border-border">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="h-9 px-2 gap-1.5 text-sm text-muted-foreground hover:text-foreground">
|
|
<User className="w-4 h-4" />
|
|
<span className="hidden lg:inline max-w-[100px] truncate">{userName}</span>
|
|
</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>
|
|
{userRole === 'TENANT_ADMIN' && (
|
|
<DropdownMenuItem onClick={() => window.location.href = '/settings'}>
|
|
<Building2 className="w-4 h-4 mr-2" />
|
|
Organisation
|
|
</DropdownMenuItem>
|
|
)}
|
|
{(userRole === 'SERVER_ADMIN' || userRole === 'TENANT_ADMIN') && (
|
|
<DropdownMenuItem onClick={() => window.location.href = '/admin'}>
|
|
<Shield className="w-4 h-4 mr-2" />
|
|
Administration
|
|
</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: overflow menu with all secondary actions */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" className="md:hidden h-9 px-2">
|
|
<MoreVertical className="w-5 h-5" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-48">
|
|
<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={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={() => setIsHoseSettingsOpen(true)}>
|
|
<Settings className="w-4 h-4 mr-2" />
|
|
Einstellungen
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={onToggleAudit}>
|
|
<ClipboardList className="w-4 h-4 mr-2" />
|
|
Audit Trail {auditLog.length > 0 && `(${auditLog.length})`}
|
|
</DropdownMenuItem>
|
|
{onLogout && (
|
|
<DropdownMenuItem onClick={onLogout} className="text-destructive">
|
|
<LogOut className="w-4 h-4 mr-2" />
|
|
Abmelden {userName && `(${userName})`}
|
|
</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>
|
|
{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">
|
|
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} />
|
|
|
|
{/* 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>
|
|
)
|
|
}
|