refactor: professional code quality audit
- 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
This commit is contained in:
1 parent
969202764a
commit
fa8dd46571
175 files changed
+24741
-2
No files matched your search
Executable
+290
@@ -0,0 +1,290 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useFetch } from '@/hooks/use-fetch'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { PageHeader } from '../primitives'
|
||||
import { useAos } from '@/store/aos'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { formatDate, formatRelative } from '@/lib/format'
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CalendarDays,
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
Landmark,
|
||||
FileSignature,
|
||||
KeyRound,
|
||||
FolderKanban,
|
||||
Receipt,
|
||||
LifeBuoy,
|
||||
CheckCircle,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface Deadline {
|
||||
id: string
|
||||
date: string
|
||||
module: string
|
||||
type: string
|
||||
title: string
|
||||
detail: string
|
||||
urgency: 'past' | 'today' | 'soon' | 'future'
|
||||
daysUntil: number
|
||||
}
|
||||
interface CalendarDay {
|
||||
date: string
|
||||
isCurrentMonth: boolean
|
||||
deadlines: Deadline[]
|
||||
}
|
||||
interface CalendarData {
|
||||
month: string
|
||||
monthLabel: string
|
||||
weeks: CalendarDay[][] // fixed typo
|
||||
upcoming: Deadline[]
|
||||
overdue: Deadline[]
|
||||
stats: { total: number; overdue: number; today: number; thisWeek: number; byModule: Record<string, number> }
|
||||
modules: string[]
|
||||
}
|
||||
|
||||
const MODULE_META: Record<string, { label: string; icon: typeof Landmark; view: string }> = {
|
||||
accounting: { label: 'Buchhaltung', icon: Landmark, view: 'accounting' },
|
||||
contracts: { label: 'Verträge', icon: FileSignature, view: 'contracts' },
|
||||
licenses: { label: 'Lizenzen', icon: KeyRound, view: 'licenses' },
|
||||
projects: { label: 'Projekte', icon: FolderKanban, view: 'projects' },
|
||||
finance: { label: 'Finanzen', icon: Receipt, view: 'finance' },
|
||||
support: { label: 'Support', icon: LifeBuoy, view: 'support' },
|
||||
portal: { label: 'Portal', icon: CheckCircle, view: 'dashboard' },
|
||||
}
|
||||
|
||||
const URGENCY_META: Record<string, { label: string; cls: string; dot: string }> = {
|
||||
past: { label: 'Überfällig', cls: 'border-foreground/60 bg-foreground/5', dot: 'bg-foreground' },
|
||||
today: { label: 'Heute', cls: 'border-foreground/60 bg-foreground/10', dot: 'bg-foreground' },
|
||||
soon: { label: 'Bald', cls: 'border-foreground/40 bg-foreground/5', dot: 'bg-foreground/70' },
|
||||
future: { label: 'Geplant', cls: 'border-border', dot: 'bg-muted-foreground/50' },
|
||||
}
|
||||
|
||||
const WEEKDAYS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
|
||||
|
||||
export function CalendarView() {
|
||||
const now = new Date()
|
||||
const [monthParam, setMonthParam] = useState(`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`)
|
||||
const { data, loading } = useFetch<CalendarData>(`/api/aos/calendar?month=${monthParam}`)
|
||||
const setView = useAos((s) => s.setView)
|
||||
|
||||
const navigateMonth = (delta: number) => {
|
||||
const [y, m] = monthParam.split('-').map(Number)
|
||||
const d = new Date(y, m - 1 + delta, 1)
|
||||
setMonthParam(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`)
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
const isToday = (dateStr: string) => {
|
||||
const d = new Date(dateStr)
|
||||
return d.getDate() === today.getDate() && d.getMonth() === today.getMonth() && d.getFullYear() === today.getFullYear()
|
||||
}
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<div className="space-y-6 p-4 md:p-6">
|
||||
<Skeleton className="h-9 w-32" />
|
||||
<Skeleton className="h-96" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { weeks, upcoming, overdue, stats, monthLabel } = data
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-4 md:p-6">
|
||||
<PageHeader
|
||||
title="Fristen"
|
||||
description="Einheitliche Kalender-Ansicht aller Deadlines: USt-Voranmeldungen, Vertrags-/Lizenzablauf, Projekt-Deadlines, Rechnungs-Fälligkeiten, Ticket-SLAs."
|
||||
/>
|
||||
|
||||
{/* KPIs */}
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 lg:grid-cols-6">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between"><p className="text-xs text-muted-foreground">Überfällig</p><AlertTriangle className="size-3.5 text-muted-foreground" /></div>
|
||||
<p className="mt-1 text-2xl font-semibold tabular-nums">{stats.overdue}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between"><p className="text-xs text-muted-foreground">Heute</p><Clock className="size-3.5 text-muted-foreground" /></div>
|
||||
<p className="mt-1 text-2xl font-semibold tabular-nums">{stats.today}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between"><p className="text-xs text-muted-foreground">Diese Woche</p><CalendarDays className="size-3.5 text-muted-foreground" /></div>
|
||||
<p className="mt-1 text-2xl font-semibold tabular-nums">{stats.thisWeek}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between"><p className="text-xs text-muted-foreground">Gesamt</p><CheckCircle2 className="size-3.5 text-muted-foreground" /></div>
|
||||
<p className="mt-1 text-2xl font-semibold tabular-nums">{stats.total}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
{/* Calendar grid */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||||
<CardTitle className="text-base">{monthLabel}</CardTitle>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" className="size-8" onClick={() => navigateMonth(-1)} aria-label="Vorheriger Monat">
|
||||
<ChevronLeft className="size-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8 text-xs" onClick={() => setMonthParam(`${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}`)}>
|
||||
Heute
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="size-8" onClick={() => navigateMonth(1)} aria-label="Nächster Monat">
|
||||
<ChevronRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{/* Weekday header */}
|
||||
<div className="grid grid-cols-7 border-y border-border bg-muted/30">
|
||||
{WEEKDAYS.map((d) => (
|
||||
<div key={d} className="px-2 py-1.5 text-center text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">{d}</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Calendar grid */}
|
||||
<div className="grid grid-cols-7">
|
||||
{weeks.flat().map((day, i) => {
|
||||
const dayDate = new Date(day.date)
|
||||
const isTodayCell = isToday(day.date)
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
'min-h-[80px] border-b border-r border-border p-1',
|
||||
!day.isCurrentMonth && 'bg-muted/20',
|
||||
(i + 1) % 7 === 0 && 'border-r-0'
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
'mb-1 flex h-5 w-5 items-center justify-center rounded text-[10px] font-medium',
|
||||
isTodayCell ? 'bg-foreground text-background' : day.isCurrentMonth ? 'text-foreground' : 'text-muted-foreground/50'
|
||||
)}>
|
||||
{dayDate.getDate()}
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{day.deadlines.slice(0, 3).map((dl) => {
|
||||
const meta = MODULE_META[dl.module] ?? { label: dl.module, icon: Clock, view: 'dashboard' }
|
||||
const Icon = meta.icon
|
||||
const urg = URGENCY_META[dl.urgency]
|
||||
return (
|
||||
<button
|
||||
key={dl.id}
|
||||
onClick={() => setView(meta.view as typeof setView extends (v: infer V) => void ? V : never)}
|
||||
className={cn('flex w-full items-center gap-1 rounded px-1 py-0.5 text-left transition hover:bg-muted', urg.cls)}
|
||||
title={dl.title}
|
||||
>
|
||||
<Icon className="size-2.5 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate text-[9px] leading-tight">{dl.title}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{day.deadlines.length > 3 && (
|
||||
<p className="px-1 text-[9px] text-muted-foreground">+{day.deadlines.length - 3} weitere</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Upcoming + Overdue list */}
|
||||
<div className="space-y-4">
|
||||
{/* Overdue */}
|
||||
{overdue.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm">
|
||||
<AlertTriangle className="size-3.5 text-foreground" />
|
||||
Überfällig ({overdue.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1.5">
|
||||
<ScrollArea className="max-h-48">
|
||||
{overdue.map((d) => <DeadlineItem key={d.id} d={d} onClick={() => setView((MODULE_META[d.module]?.view ?? 'dashboard') as never)} />)}
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Upcoming */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm">
|
||||
<Clock className="size-3.5 text-muted-foreground" />
|
||||
Anstehend ({upcoming.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1.5">
|
||||
<ScrollArea className="max-h-96">
|
||||
{upcoming.length === 0 && <p className="py-4 text-center text-xs text-muted-foreground">Keine anstehenden Fristen.</p>}
|
||||
{upcoming.map((d) => <DeadlineItem key={d.id} d={d} onClick={() => setView((MODULE_META[d.module]?.view ?? 'dashboard') as never)} />)}
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* By module */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Verteilung nach Modul</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1.5">
|
||||
{Object.entries(stats.byModule).map(([mod, count]) => {
|
||||
const meta = MODULE_META[mod] ?? { label: mod, icon: Clock, view: 'dashboard' }
|
||||
const Icon = meta.icon
|
||||
return (
|
||||
<div key={mod} className="flex items-center gap-2 text-xs">
|
||||
<Icon className="size-3 text-muted-foreground" />
|
||||
<span className="flex-1">{meta.label}</span>
|
||||
<Badge variant="outline" className="text-[10px]">{count}</Badge>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DeadlineItem({ d, onClick }: { d: Deadline; onClick: () => void }) {
|
||||
const meta = MODULE_META[d.module] ?? { label: d.module, icon: Clock, view: 'dashboard' }
|
||||
const Icon = meta.icon
|
||||
const urg = URGENCY_META[d.urgency]
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn('flex w-full items-start gap-2 rounded-md border p-2 text-left transition hover:bg-muted/50', urg.cls)}
|
||||
>
|
||||
<Icon className="mt-0.5 size-3 shrink-0 text-muted-foreground" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-[11px] font-medium">{d.title}</p>
|
||||
<p className="truncate text-[10px] text-muted-foreground">{d.detail}</p>
|
||||
<div className="mt-0.5 flex items-center gap-1.5 text-[9px] text-muted-foreground">
|
||||
<span className="flex items-center gap-0.5">
|
||||
<span className={cn('size-1 rounded-full', urg.dot)} />
|
||||
{d.type}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>{formatDate(d.date)}</span>
|
||||
<span>·</span>
|
||||
<span className={cn(d.daysUntil < 0 && 'text-foreground font-medium')}>
|
||||
{d.daysUntil === 0 ? 'heute' : d.daysUntil < 0 ? `${Math.abs(d.daysUntil)} d drüber` : `in ${d.daysUntil} d`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user