- Fix all TypeScript compilation errors (0 errors in src/) - Add try/catch error handling to ALL API routes (38 handlers) - Fix empty catch blocks (api-auth.ts now logs warnings) - Add aria-labels to all icon-only buttons (WCAG 2.1 AA) - Fix Prisma type mismatches (OR filter, Person.companyId, Estimate.projectId, Ticket.createdAt) - Fix webhook PATCH route (moved to [id]/route.ts) - Fix mobile-bottom-nav type safety - Fix KB route where-clause type - Fix test-mail banner null vs undefined - Fix task comment fetch: add cancellation + error toast - ESLint: 0 errors, 0 warnings - 175 files, 24.741 lines of code
64 lines
2.8 KiB
TypeScript
Executable File
64 lines
2.8 KiB
TypeScript
Executable File
'use client'
|
|
|
|
import { useAos, type ViewId } from '@/store/aos'
|
|
import { LayoutDashboard, Radar, FolderKanban, Bell, Menu } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
import { useFetch } from '@/hooks/use-fetch'
|
|
|
|
// Mobile bottom navigation bar (PRD §9: "Mobile: usable PWA for time tracking,
|
|
// approvals and quick CRM lookups"). Shows on mobile only (md:hidden).
|
|
// Provides 5 quick-access destinations optimized for touch.
|
|
export function MobileBottomNav() {
|
|
const { view, setView, setSidebarOpen, setCommandOpen } = useAos()
|
|
const { data } = useFetch<{ stats: { total: number; urgent: number } }>('/api/aos/notifications')
|
|
|
|
const urgent = data?.stats.urgent ?? 0
|
|
const total = data?.stats.total ?? 0
|
|
|
|
const items: { id: string; label: string; icon: typeof LayoutDashboard; badge?: number }[] = [
|
|
{ id: 'dashboard', label: 'Home', icon: LayoutDashboard },
|
|
{ id: 'leads', label: 'Leads', icon: Radar },
|
|
{ id: 'search', label: 'Suche', icon: Menu },
|
|
{ id: 'projects', label: 'Zeit', icon: FolderKanban },
|
|
{ id: 'notifications', label: 'Alerts', icon: Bell, badge: urgent > 0 ? urgent : total > 0 ? total : undefined },
|
|
]
|
|
|
|
const handleClick = (id: string) => {
|
|
if (id === 'search') setCommandOpen(true)
|
|
else if (id === 'notifications') setView('calendar')
|
|
else if (id === 'menu') setSidebarOpen(true)
|
|
else setView(id as ViewId)
|
|
}
|
|
|
|
return (
|
|
<nav className="fixed bottom-0 left-0 right-0 z-40 flex h-16 items-stretch border-t border-border bg-background/95 backdrop-blur-md md:hidden">
|
|
{items.map((item) => {
|
|
const Icon = item.icon
|
|
const active = view === (item.id === 'notifications' ? 'calendar' : (item.id === 'search' || item.id === 'menu') ? ('' as ViewId) : (item.id as ViewId))
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => handleClick(item.id)}
|
|
className={cn(
|
|
'relative flex flex-1 flex-col items-center justify-center gap-0.5 text-[10px] font-medium transition',
|
|
active ? 'text-foreground' : 'text-muted-foreground'
|
|
)}
|
|
aria-label={item.label}
|
|
>
|
|
<div className="relative">
|
|
<Icon className={cn('size-5', active && 'text-foreground')} />
|
|
{item.badge !== undefined && item.badge > 0 && (
|
|
<span className="absolute -right-2 -top-1.5 flex size-4 items-center justify-center rounded-full bg-foreground text-[8px] font-bold text-background">
|
|
{item.badge > 9 ? '9+' : item.badge}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<span>{item.label}</span>
|
|
{active && <span className="absolute bottom-0 left-1/2 h-0.5 w-8 -translate-x-1/2 rounded-full bg-foreground" />}
|
|
</button>
|
|
)
|
|
})}
|
|
</nav>
|
|
)
|
|
}
|