'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { signOut, useSession } from "next-auth/react"; import { usePathname, useRouter } from "next/navigation"; import { LayoutDashboard, Users, Ticket, LogOut, Shield, ShieldCheck, FileText, Settings, UserCircle, Search, Key, Menu, X, ChevronRight, Building2 } from "lucide-react"; // ────────────────────────────────────────────── // STATUS TRANSLATION HELPER // ────────────────────────────────────────────── export const statusLabels: Record = { OPEN: 'Offen', IN_PROGRESS: 'In Bearbeitung', WAITING_FOR_CUSTOMER: 'Wartet auf Kunde', RESOLVED: 'Gelöst', CLOSED: 'Geschlossen', }; export const priorityLabels: Record = { LOW: 'Niedrig', MEDIUM: 'Mittel', HIGH: 'Hoch', CRITICAL: 'Kritisch', }; export const priorityColors: Record = { LOW: 'bg-slate-100 text-slate-600 border-slate-200', MEDIUM: 'bg-blue-50 text-blue-600 border-blue-200', HIGH: 'bg-orange-50 text-orange-600 border-orange-200', CRITICAL: 'bg-red-50 text-red-700 border-red-200', }; export function getStatusBadge(status: string) { const label = statusLabels[status] || status; const colors: Record = { OPEN: 'text-red-700 bg-red-50 border-red-200', IN_PROGRESS: 'text-amber-700 bg-amber-50 border-amber-200', WAITING_FOR_CUSTOMER: 'text-indigo-700 bg-indigo-50 border-indigo-200', RESOLVED: 'text-emerald-700 bg-emerald-50 border-emerald-200', CLOSED: 'text-slate-700 bg-slate-50 border-slate-200', }; return ( {label} ); } export function getPriorityBadge(priority: string) { const label = priorityLabels[priority] || priority; const color = priorityColors[priority] || priorityColors.MEDIUM; return ( {label} ); } // ────────────────────────────────────────────── // BREADCRUMBS // ────────────────────────────────────────────── const breadcrumbLabels: Record = { '': 'Übersicht', customers: 'Kunden', tickets: 'Tickets', billing: 'Abrechnung', products: 'Produkte', sales: 'Verkauf', users: 'Team', roles: 'Rechte', settings: 'Einstellungen', search: 'Suche', new: 'Neu', }; function Breadcrumbs() { const pathname = usePathname(); const segments = pathname.split('/').filter(Boolean); if (segments.length === 0) return null; const crumbs = segments.map((seg, idx) => { const href = '/' + segments.slice(0, idx + 1).join('/'); const isLast = idx === segments.length - 1; const label = breadcrumbLabels[seg] || (seg.startsWith('[') ? seg : `#${seg}`); return ( {isLast ? ( {label} ) : ( {label} )} ); }); return (
Start {crumbs}
); } // ────────────────────────────────────────────── // FORCE PASSWORD CHANGE FORM (CUSTOMER) // ────────────────────────────────────────────── function ForcePasswordChangeForm() { const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(''); const res = await fetch('/api/portal/password', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); if (res.ok) { signOut({ callbackUrl: '/login' }); } else { setError('Passwort muss mindestens 6 Zeichen lang sein.'); setLoading(false); } }; return (

Sicherheitshinweis

Aus Sicherheitsgründen musst du dein initiales Passwort ändern, bevor du das Portal nutzen kannst.

{error &&
{error}
}
setPassword(e.target.value)} />
); } // ────────────────────────────────────────────── // MAIN APP SHELL (Sidebar + Header + Content) // ────────────────────────────────────────────── export default function AppShell({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const router = useRouter(); const { data: session, status } = useSession(); const [searchQuery, setSearchQuery] = useState(''); const [sidebarOpen, setSidebarOpen] = useState(false); const [liveResults, setLiveResults] = useState([]); const [showLiveResults, setShowLiveResults] = useState(false); const [liveLoading, setLiveLoading] = useState(false); const searchRef = useRef(null); const debounceRef = useRef(null); // Close dropdown on click outside useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (searchRef.current && !searchRef.current.contains(e.target as Node)) { setShowLiveResults(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // Debounced live search const handleSearchInput = useCallback((value: string) => { setSearchQuery(value); if (debounceRef.current) clearTimeout(debounceRef.current); if (value.trim().length < 2) { setLiveResults([]); setShowLiveResults(false); return; } debounceRef.current = setTimeout(async () => { setLiveLoading(true); try { const res = await fetch(`/api/customers/search?q=${encodeURIComponent(value)}`); if (res.ok) { const data = await res.json(); setLiveResults(data); setShowLiveResults(true); } } catch { /* ignore */ } setLiveLoading(false); }, 300); }, []); const isLoginPage = pathname === "/login"; if (isLoginPage) return
{children}
; if (status === "loading") { return (

Lade System...

); } const userType = (session?.user as any)?.userType; const permissions = (session?.user as any)?.permissions || []; const canManageTeam = permissions.includes('TEAM_MANAGE'); const canManageSettings = permissions.includes('SYSTEM_SETTINGS'); // ── CUSTOMER PORTAL ── if (userType === 'CUSTOMER') { if ((session?.user as any)?.forcePasswordChange) { return ; } return (
P
Kundenportal
{(session?.user as any)?.firstName} {(session?.user as any)?.lastName}
{children}
); } const canPurchasing = permissions.includes('PURCHASING_MANAGE'); const canSales = permissions.includes('SALES_MANAGE'); // ── TEAM LAYOUT ── const navCategories = [ { title: 'Übersicht & CRM', items: [ { name: 'Dashboard', href: '/', icon: LayoutDashboard }, { name: 'Kunden', href: '/customers', icon: Users }, ] }, { title: 'Service & Abrechnung', items: [ { name: 'Tickets', href: '/tickets', icon: Ticket }, ...(canManageTeam ? [{ name: 'Abrechnung', href: '/billing', icon: FileText }] : []) ] }, { title: 'Warenwirtschaft', items: [ ...(canPurchasing ? [{ name: 'Produkte', href: '/products', icon: Building2 }] : []), ...(canSales ? [{ name: 'Verkauf', href: '/sales', icon: FileText }] : []) ] }, { title: 'Administration', items: [ ...(canManageTeam ? [ { name: 'Team', href: '/users', icon: Shield }, { name: 'Rechte', href: '/roles', icon: ShieldCheck } ] : []), ...(canManageSettings ? [{ name: 'Einstellungen', href: '/settings', icon: Settings }] : []) ] } ]; const handleSearch = (e: React.FormEvent) => { e.preventDefault(); if (searchQuery.trim()) { router.push(`/search?q=${encodeURIComponent(searchQuery)}`); setSidebarOpen(false); } }; return (
{/* Mobile Overlay */} {sidebarOpen && (
setSidebarOpen(false)} /> )} {/* Sidebar */} {/* Main Content Area */}
handleSearchInput(e.target.value)} onFocus={() => { if (liveResults.length > 0) setShowLiveResults(true); }} className="pl-9 pr-4 py-2 bg-slate-100/80 border border-slate-200 focus:bg-white focus:border-indigo-400 focus:ring-4 focus:ring-indigo-500/10 rounded-xl text-sm outline-none transition-all w-48 md:w-72 shadow-sm" /> {/* Live Search Dropdown */} {showLiveResults && (
{liveLoading ? (
Suche...
) : liveResults.length === 0 ? (
Keine Kunden gefunden
) : (
    {liveResults.map((c: any) => (
  • ))}
)}
)}
{children}
); }