Files
KanzlAI-mGMT/frontend/src/components/layout/Sidebar.tsx
m fdef5af32e feat: reporting dashboard — case stats, deadline compliance, workload, billing (P1)
Backend:
- ReportingService with aggregation queries (CTEs, FILTER clauses)
- 4 API endpoints: /api/reports/{cases,deadlines,workload,billing}
- Date range filtering via ?from=&to= query params

Frontend:
- /berichte page with 4 tabs: Akten, Fristen, Auslastung, Abrechnung
- recharts: bar/pie/line charts for all report types
- Date range picker, CSV export, print-friendly view
- Sidebar nav entry with BarChart3 icon

Also resolves merge conflicts between role-based, notification, and
audit trail branches, and adds missing TS types (AuditLogResponse,
Notification, NotificationPreferences).
2026-03-30 11:24:45 +02:00

127 lines
3.8 KiB
TypeScript

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard,
FolderOpen,
Clock,
Calendar,
Brain,
BarChart3,
Settings,
Menu,
X,
} from "lucide-react";
import { useState, useEffect } from "react";
import { usePermissions } from "@/lib/hooks/usePermissions";
interface NavItem {
name: string;
href: string;
icon: typeof LayoutDashboard;
permission?: string;
}
const allNavigation: NavItem[] = [
{ name: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
{ name: "Akten", href: "/cases", icon: FolderOpen },
{ name: "Fristen", href: "/fristen", icon: Clock },
{ name: "Termine", href: "/termine", icon: Calendar },
{ name: "Berichte", href: "/berichte", icon: BarChart3 },
{ name: "AI Analyse", href: "/ai/extract", icon: Brain, permission: "ai_extraction" },
{ name: "Einstellungen", href: "/einstellungen", icon: Settings, permission: "manage_settings" },
];
export function Sidebar() {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
const { can, isLoading: permLoading } = usePermissions();
const navigation = allNavigation.filter(
(item) => !item.permission || permLoading || can(item.permission),
);
// Close on route change
useEffect(() => {
setMobileOpen(false);
}, [pathname]);
// Close on escape
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") setMobileOpen(false);
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, []);
const navContent = (
<>
<div className="flex h-14 items-center justify-between border-b border-neutral-200 px-4">
<span className="text-sm font-semibold text-neutral-900">KanzlAI</span>
<button
onClick={() => setMobileOpen(false)}
className="rounded-md p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600 lg:hidden"
aria-label="Menü schließen"
>
<X className="h-4 w-4" />
</button>
</div>
<nav className="flex-1 space-y-0.5 p-2">
{navigation.map((item) => {
const isActive =
pathname === item.href || pathname.startsWith(item.href + "/");
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-2.5 rounded-md px-2.5 py-2 text-sm transition-colors ${
isActive
? "bg-neutral-100 font-medium text-neutral-900"
: "text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900"
}`}
>
<item.icon className="h-4 w-4 shrink-0" />
{item.name}
</Link>
);
})}
</nav>
</>
);
return (
<>
{/* Mobile hamburger button */}
<button
onClick={() => setMobileOpen(true)}
className="fixed left-3 top-3.5 z-40 rounded-md bg-white p-1.5 shadow-sm ring-1 ring-neutral-200 transition-colors hover:bg-neutral-50 lg:hidden"
aria-label="Menü öffnen"
>
<Menu className="h-5 w-5 text-neutral-700" />
</button>
{/* Mobile overlay */}
{mobileOpen && (
<div
className="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm lg:hidden"
onClick={() => setMobileOpen(false)}
/>
)}
{/* Mobile sidebar */}
{mobileOpen && (
<aside className="animate-slide-in-left fixed inset-y-0 left-0 z-50 flex w-56 flex-col border-r border-neutral-200 bg-white shadow-lg lg:hidden">
{navContent}
</aside>
)}
{/* Desktop sidebar */}
<aside className="hidden h-full w-56 flex-col border-r border-neutral-200 bg-white lg:flex">
{navContent}
</aside>
</>
);
}