mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 17:13:42 +00:00
197 lines
7.1 KiB
TypeScript
197 lines
7.1 KiB
TypeScript
import { useNavigate } from 'react-router-dom'
|
|
import { useAuth } from '@/context/AuthContext'
|
|
import { useNotifications } from '@/hooks/useNotifications'
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from '@/components/ui/popover'
|
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
import { Separator } from '@/components/ui/separator'
|
|
import { Bell, CheckCheck, LogOut, Menu, Moon, Sun, UserCog } from 'lucide-react'
|
|
import { useTheme } from '@/hooks/useTheme'
|
|
|
|
interface HeaderProps {
|
|
onToggleSidebar: () => void
|
|
}
|
|
|
|
export function Header({ onToggleSidebar }: HeaderProps) {
|
|
const navigate = useNavigate()
|
|
const { user, logout } = useAuth()
|
|
const { notifications, unreadCount, markAsRead, markAllAsRead } = useNotifications()
|
|
const { theme, toggleTheme } = useTheme()
|
|
|
|
const initials = user?.username
|
|
? user.username
|
|
.split(/[\s._-]/)
|
|
.map((part) => part[0])
|
|
.join('')
|
|
.toUpperCase()
|
|
.slice(0, 2)
|
|
: '??'
|
|
|
|
const roleBadgeVariant = user?.role === 'admin' ? 'default' as const : 'secondary' as const
|
|
const roleLabel = user?.role === 'admin' ? 'Admin' : 'DAK Mitarbeiter'
|
|
|
|
return (
|
|
<header className="flex h-14 items-center gap-4 border-b bg-background px-4 lg:px-6">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="lg:hidden"
|
|
onClick={onToggleSidebar}
|
|
>
|
|
<Menu className="h-5 w-5" />
|
|
<span className="sr-only">Navigation umschalten</span>
|
|
</Button>
|
|
|
|
<div className="flex-1" />
|
|
|
|
{/* Dark mode toggle */}
|
|
<Button variant="ghost" size="icon" onClick={toggleTheme}>
|
|
{theme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
|
<span className="sr-only">Design umschalten</span>
|
|
</Button>
|
|
|
|
{/* Notification bell with dropdown */}
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="relative">
|
|
<Bell className="h-5 w-5" />
|
|
{unreadCount > 0 && (
|
|
<span className="absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-bold text-destructive-foreground">
|
|
{unreadCount > 99 ? '99+' : unreadCount}
|
|
</span>
|
|
)}
|
|
<span className="sr-only">Benachrichtigungen</span>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent align="end" className="w-80 p-0">
|
|
<div className="flex items-center justify-between px-4 py-3">
|
|
<h4 className="text-sm font-semibold">Benachrichtigungen</h4>
|
|
{unreadCount > 0 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-auto px-2 py-1 text-xs"
|
|
onClick={markAllAsRead}
|
|
>
|
|
<CheckCheck className="mr-1 h-3 w-3" />
|
|
Alle als gelesen markieren
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<Separator />
|
|
<ScrollArea className="max-h-80">
|
|
{notifications.length === 0 ? (
|
|
<p className="px-4 py-6 text-center text-sm text-muted-foreground">
|
|
Keine Benachrichtigungen
|
|
</p>
|
|
) : (
|
|
<div className="flex flex-col">
|
|
{notifications.map((n) => (
|
|
<button
|
|
key={n.id}
|
|
className={`flex flex-col gap-1 px-4 py-3 text-left transition-colors hover:bg-muted/50 ${
|
|
!n.is_read ? 'bg-muted/30' : ''
|
|
}`}
|
|
onClick={() => {
|
|
if (!n.is_read) markAsRead(n.id)
|
|
}}
|
|
>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<span className="text-sm font-medium leading-tight">
|
|
{!n.is_read && (
|
|
<span className="mr-1.5 inline-block h-2 w-2 rounded-full bg-primary" />
|
|
)}
|
|
{n.title}
|
|
</span>
|
|
</div>
|
|
{n.message && (
|
|
<span className="text-xs text-muted-foreground line-clamp-2">
|
|
{n.message}
|
|
</span>
|
|
)}
|
|
<span className="text-[11px] text-muted-foreground">
|
|
{formatRelativeTime(n.created_at)}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</PopoverContent>
|
|
</Popover>
|
|
|
|
{/* User menu */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="flex items-center gap-2 px-2">
|
|
<Avatar size="sm">
|
|
{user?.avatar_url && <AvatarImage src={user.avatar_url} />}
|
|
<AvatarFallback>{initials}</AvatarFallback>
|
|
</Avatar>
|
|
<span className="hidden text-sm font-medium md:inline-block">
|
|
{user?.username}
|
|
</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<DropdownMenuLabel className="font-normal">
|
|
<div className="flex flex-col space-y-1">
|
|
<p className="text-sm font-medium leading-none">{user?.username}</p>
|
|
<p className="text-xs leading-none text-muted-foreground">{user?.email}</p>
|
|
<Badge variant={roleBadgeVariant} className="mt-1 w-fit">
|
|
{roleLabel}
|
|
</Badge>
|
|
</div>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => navigate('/account')} className="cursor-pointer">
|
|
<UserCog className="mr-2 h-4 w-4" />
|
|
Konto verwalten
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => logout()} className="cursor-pointer">
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
Abmelden
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</header>
|
|
)
|
|
}
|
|
|
|
function formatRelativeTime(dateStr: string): string {
|
|
try {
|
|
const date = new Date(dateStr)
|
|
const now = new Date()
|
|
const diffMs = now.getTime() - date.getTime()
|
|
const diffMin = Math.floor(diffMs / 60_000)
|
|
const diffHrs = Math.floor(diffMs / 3_600_000)
|
|
const diffDays = Math.floor(diffMs / 86_400_000)
|
|
|
|
if (diffMin < 1) return 'Gerade eben'
|
|
if (diffMin < 60) return `vor ${diffMin} Min.`
|
|
if (diffHrs < 24) return `vor ${diffHrs} Std.`
|
|
if (diffDays < 7) return `vor ${diffDays} Tag${diffDays > 1 ? 'en' : ''}`
|
|
return date.toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
})
|
|
} catch {
|
|
return dateStr
|
|
}
|
|
}
|