Files
AOS/src/components/aos/views/finance.tsx
T
Z User fa8dd46571 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
2026-10-27 12:23:32 +00:00

246 lines
10 KiB
TypeScript
Executable File

'use client'
import { useState } from 'react'
import {
Plus,
Receipt,
FileCheck2,
AlertTriangle,
Download,
FileText,
Euro,
Clock,
} from 'lucide-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 { PageHeader } from '../primitives'
import { InvoiceDetailDialog, type Invoice } from '../invoice-dialog'
import { formatEuro, formatDate, daysUntil } from '@/lib/format'
import { cn } from '@/lib/utils'
interface FinanceData {
invoices: Invoice[]
stats: {
total: number
draft: number
sent: number
paid: number
overdue: number
zugferdCount: number
xrechnungCount: number
datevCount: number
outstandingCents: number
}
}
const STATUS_META: Record<string, { label: string; cls: string }> = {
draft: { label: 'Entwurf', cls: 'bg-muted text-foreground' },
sent: { label: 'Versendet', cls: 'bg-foreground/70 text-background' },
paid: { label: 'Bezahlt', cls: 'bg-foreground text-background' },
overdue: { label: 'Überfällig', cls: 'bg-foreground text-background' },
cancelled: { label: 'Storniert', cls: 'bg-muted text-muted-foreground' },
storno: { label: 'Storno', cls: 'bg-muted text-muted-foreground' },
}
const TYPE_LABELS: Record<string, string> = {
final: 'Schlussrechnung',
partial: 'Abschlag',
recurring: 'Wiederkehrend',
credit: 'Gutschrift',
}
export function FinanceView() {
const { data, loading, refetch } = useFetch<FinanceData>('/api/aos/finance')
const [selected, setSelected] = useState<Invoice | null>(null)
const [dialogOpen, setDialogOpen] = useState(false)
if (loading || !data) {
return (
<div className="space-y-6 p-4 md:p-6">
<Skeleton className="h-9 w-32" />
<Skeleton className="h-48" />
</div>
)
}
const { invoices, stats } = data
return (
<div className="space-y-6 p-4 md:p-6">
<PageHeader
title="Finanzen"
description="Rechnungen mit lückenloser Nummerierung, E-Rechnung (ZUGFeRD/XRechnung), Mahnwesen und DATEV-Export."
actions={
<Button size="sm">
<Plus className="size-4" />
Rechnung
</Button>
}
/>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">Offene Forderungen</p>
<Euro className="size-3.5 text-muted-foreground" />
</div>
<p className="mt-1 text-2xl font-semibold tabular-nums">{formatEuro(stats.outstandingCents)}</p>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">Rechnungen gesamt</p>
<Receipt className="size-3.5 text-muted-foreground" />
</div>
<p className="mt-1 text-2xl font-semibold tabular-nums">{stats.total}</p>
<p className="text-[10px] text-muted-foreground">{stats.draft} Entwurf · {stats.overdue} überfällig</p>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">ZUGFeRD / XRechnung</p>
<FileCheck2 className="size-3.5 text-muted-foreground" />
</div>
<p className="mt-1 text-2xl font-semibold tabular-nums">{stats.zugferdCount} / {stats.xrechnungCount}</p>
<p className="text-[10px] text-muted-foreground">E-Rechnung-konform</p>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">DATEV exportiert</p>
<Download className="size-3.5 text-muted-foreground" />
</div>
<p className="mt-1 text-2xl font-semibold tabular-nums">{stats.datevCount}</p>
</Card>
</div>
{/* Compliance notice */}
<div className="flex flex-col gap-2 rounded-md border border-border bg-muted/30 p-3 text-xs sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
<div>
<p className="font-medium">E-Rechnung-Pflicht (ab 2025 Empfang, ab 2027/2028 Versand je nach Umsatz).</p>
<p className="text-muted-foreground">Validierung gem. EN 16931 über <code className="rounded bg-muted px-1 py-0.5 font-mono text-[10px]">@e-invoice-eu/core</code>. GoBD: Rechnungen 8 Jahre, Geschäftsbriefe 6 Jahre (mit Steuerberater bestätigen).</p>
</div>
</div>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">Rechnungen</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="border-y border-border bg-muted/40 text-left text-[11px] uppercase tracking-wide text-muted-foreground">
<tr>
<th className="px-4 py-2 font-medium">Nr.</th>
<th className="px-4 py-2 font-medium">Kunde</th>
<th className="px-4 py-2 font-medium">Typ</th>
<th className="px-4 py-2 font-medium">Datum</th>
<th className="px-4 py-2 font-medium">Fällig</th>
<th className="px-4 py-2 font-medium text-right">Netto</th>
<th className="px-4 py-2 font-medium text-right">Brutto</th>
<th className="px-4 py-2 font-medium">E-Rechnung</th>
<th className="px-4 py-2 font-medium">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{invoices.map((inv) => {
const st = STATUS_META[inv.status] ?? STATUS_META.draft
const dueDays = daysUntil(inv.dueDate)
return (
<tr
key={inv.id}
className="cursor-pointer transition hover:bg-muted/40"
onClick={() => { setSelected(inv); setDialogOpen(true) }}
>
<td className="px-4 py-2.5">
<code className="font-mono text-[11px]">{inv.number}</code>
</td>
<td className="px-4 py-2.5 text-xs">{inv.company.name}</td>
<td className="px-4 py-2.5 text-xs text-muted-foreground">{TYPE_LABELS[inv.type] ?? inv.type}</td>
<td className="px-4 py-2.5 text-xs">{formatDate(inv.issueDate)}</td>
<td className="px-4 py-2.5 text-xs">
{formatDate(inv.dueDate)}
{inv.status === 'sent' && dueDays < 0 && (
<span className="ml-1 text-[10px] text-foreground">({Math.abs(dueDays)} d überfällig)</span>
)}
</td>
<td className="px-4 py-2.5 text-right text-xs tabular-nums">{formatEuro(inv.netCents)}</td>
<td className="px-4 py-2.5 text-right text-xs font-semibold tabular-nums">{formatEuro(inv.grossCents)}</td>
<td className="px-4 py-2.5">
<div className="flex gap-1">
{inv.zugferd && (
<Badge variant="outline" className="text-[9px]">ZUGFeRD</Badge>
)}
{inv.xrechnung && (
<Badge variant="outline" className="text-[9px]">XRechnung</Badge>
)}
{inv.datevExported && (
<Badge variant="secondary" className="text-[9px]">DATEV</Badge>
)}
{!inv.zugferd && !inv.xrechnung && !inv.datevExported && (
<span className="text-[10px] text-muted-foreground">—</span>
)}
</div>
</td>
<td className="px-4 py-2.5">
<Badge variant="outline" className={cn('text-[10px]', st.cls)}>
{st.label}
</Badge>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* Gapless numbering + finalize info */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Card>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<div className="flex size-9 items-center justify-center rounded-md border border-border">
<FileText className="size-4 text-muted-foreground" />
</div>
<div className="flex-1">
<p className="text-sm font-medium">Lückenlose Nummerierung</p>
<p className="mt-1 text-xs text-muted-foreground">
RE-2026-0042 → RE-2026-0043 … GoBD-konform, kein Löschen, Korrekturen als Storno (neue Zeile).
Finalisierungs-Sperre verhindert Nachträgliches Editieren.
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<div className="flex size-9 items-center justify-center rounded-md border border-border">
<Clock className="size-4 text-muted-foreground" />
</div>
<div className="flex-1">
<p className="text-sm font-medium">Time-to-Invoice: 0 manuelle Schritte</p>
<p className="mt-1 text-xs text-muted-foreground">
Freigegebene Zeiteinträge werden direkt zu Rechnungspositionen. Quote → Projekt-Budget → Rechnungsplan → DATEV-Export.
</p>
</div>
</div>
</CardContent>
</Card>
</div>
<InvoiceDetailDialog
invoice={selected}
open={dialogOpen}
onOpenChange={setDialogOpen}
onFinalized={refetch}
/>
</div>
)
}