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).
127 lines
3.8 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|