Initial commit: Lageplan v1.0 - Next.js 15.5, React 19
This commit is contained in:
542
src/components/layout/topbar.tsx
Normal file
542
src/components/layout/topbar.tsx
Normal file
@@ -0,0 +1,542 @@
|
||||
'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,
|
||||
} 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 [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>
|
||||
</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={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>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user