diff --git a/db/custom.db b/db/custom.db index 9d0292f..9ac5aeb 100644 Binary files a/db/custom.db and b/db/custom.db differ diff --git a/tool-results/read_1793121909886_c9de18dc46a4.txt b/tool-results/read_1793121909886_c9de18dc46a4.txt new file mode 100644 index 0000000..25296ce --- /dev/null +++ b/tool-results/read_1793121909886_c9de18dc46a4.txt @@ -0,0 +1,662 @@ + 1→'use client' + 2→ + 3→import { + 4→ BookOpen, + 5→ Landmark, + 6→ TrendingUp, + 7→ ArrowDownLeft, + 8→ ArrowUpRight, + 9→ Send, + 10→ CheckCircle2, + 11→ AlertTriangle, + 12→ Loader2, + 13→ Link2, + 14→ Banknote, + 15→} from 'lucide-react' + 16→import { useFetch } from '@/hooks/use-fetch' + 17→import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' + 18→import { Badge } from '@/components/ui/badge' + 19→import { Button } from '@/components/ui/button' + 20→import { Skeleton } from '@/components/ui/skeleton' + 21→import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' + 22→import { PageHeader } from '../primitives' + 23→import { formatEuro, formatEuroCompact, formatDate, formatDateTime } from '@/lib/format' + 24→import { cn } from '@/lib/utils' + 25→import { toast } from 'sonner' + 26→import { useState } from 'react' + 27→import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis, Cell, Line, LineChart, CartesianGrid } from 'recharts' + 28→ + 29→interface VatReturn { + 30→ id: string + 31→ periodYear: number + 32→ periodMonth: number + 33→ periodType: string + 34→ status: string + 35→ k81StandardNetCents: number + 36→ k86ReducedNetCents: number + 37→ k66TaxFreeCents: number + 38→ k83TaxStandardCents: number + 39→ k65TaxReducedCents: number + 40→ k37InputTaxCents: number + 41→ outputTaxCents: number + 42→ inputTaxCents: number + 43→ payableCents: number + 44→ submittedAt: string | null + 45→ elsterRef: string | null + 46→ acceptedAt: string | null + 47→ notes: string | null + 48→} + 49→interface BankTransaction { + 50→ id: string + 51→ amountCents: number + 52→ bookingDate: string + 53→ valueDate: string + 54→ partnerName: string | null + 55→ partnerIban: string | null + 56→ reference: string | null + 57→ purpose: string | null + 58→ matchedStatus: string + 59→ matchReason: string | null + 60→} + 61→interface AccountingData { + 62→ vat: { + 63→ current: VatReturn | null + 64→ history: VatReturn[] + 65→ totalPayableYTD: number + 66→ stats: { periods: number; accepted: number; pending: number } + 67→ } + 68→ eur: { + 69→ months: { label: string; incomeCents: number; costCents: number; profitCents: number }[] + 70→ totalIncome: number + 71→ totalCost: number + 72→ totalProfit: number + 73→ costsByCategory: { category: string; cents: number }[] + 74→ } + 75→ bank: { + 76→ transactions: BankTransaction[] + 77→ stats: { matched: number; suggested: number; unmatched: number; inflowCents: number; outflowCents: number; total: number } + 78→ } + 79→ dunning: { + 80→ overdue: { id: string; number: string; companyName: string; grossCents: number; dueDate: string; daysOverdue: number }[] + 81→ runs: { id: string; level: number; status: string; feesCents: number; sentAt: string; invoice: { number: string; company: { name: string } } }[] + 82→ stats: { overdueCount: number; overdueCents: number; dunningSent: number } + 83→ } + 84→} + 85→ + 86→const VAT_STATUS_META: Record = { + 87→ draft: { label: 'Entwurf', cls: 'bg-muted text-foreground' }, + 88→ computed: { label: 'Berechnet', cls: 'bg-foreground/50 text-background' }, + 89→ prepared: { label: 'XML erstellt', cls: 'bg-foreground/70 text-background' }, + 90→ submitted: { label: 'Übermittelt', cls: 'bg-foreground/70 text-background' }, + 91→ accepted: { label: 'Akzeptiert', cls: 'bg-foreground text-background' }, + 92→ corrected: { label: 'Korrigiert', cls: 'bg-muted text-muted-foreground' }, + 93→} + 94→ + 95→const MONTH_NAMES = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'] + 96→ + 97→const CATEGORY_LABELS: Record = { + 98→ reise: 'Reise', software: 'Software', hardware: 'Hardware', fremdleistung: 'Fremdleistung', buero: 'Büro', marketing: 'Marketing', + 99→} + 100→ + 101→export function AccountingView() { + 102→ const { data, loading, refetch } = useFetch('/api/aos/accounting') + 103→ const [submitting, setSubmitting] = useState(false) + 104→ const [preparedXml, setPreparedXml] = useState(null) + 105→ + 106→ if (loading || !data) { + 107→ return ( + 108→
+ 109→ + 110→ + 111→
+ 112→ ) + 113→ } + 114→ + 115→ const { vat, eur, bank, dunning } = data + 116→ + 117→ const submitVat = async (vatId: string) => { + 118→ setSubmitting(true) + 119→ setPreparedXml(null) + 120→ const t = toast.loading('UStVA-XML wird erstellt und validiert…') + 121→ try { + 122→ const r = await fetch(`/api/aos/accounting/vat/${vatId}/submit`, { method: 'PATCH' }) + 123→ const res = await r.json() + 124→ if (!r.ok) { + 125→ if (res.details && Array.isArray(res.details)) { + 126→ throw new Error('Validierung fehlgeschlagen: ' + res.details.join('; ')) + 127→ } + 128→ throw new Error(res.error || 'submit failed') + 129→ } + 130→ if (res.transmitted === false) { + 131→ // Honest path: XML prepared, transmission requires ERiC certificate + 132→ setPreparedXml(res.xml) + 133→ toast.success(`UStVA-XML erstellt (${res.xmlSize} Bytes) — Übermittlung an ERiC ausstehend`, { id: t, duration: 6000 }) + 134→ } else { + 135→ toast.success('An ELSTER übermittelt', { id: t }) + 136→ } + 137→ refetch() + 138→ } catch (err) { + 139→ toast.error(err instanceof Error ? err.message : 'Übermittlung fehlgeschlagen', { id: t }) + 140→ } finally { + 141→ setSubmitting(false) + 142→ } + 143→ } + 144→ + 145→ const matchTx = async (txId: string, invoiceId: string) => { + 146→ const t = toast.loading('Bankbewegung wird zugeordnet…') + 147→ try { + 148→ const r = await fetch(`/api/aos/accounting/bank/${txId}/match`, { + 149→ method: 'POST', + 150→ headers: { 'content-type': 'application/json' }, + 151→ body: JSON.stringify({ invoiceId }), + 152→ }) + 153→ if (!r.ok) throw new Error('match failed') + 154→ toast.success('Zugeordnet — Rechnung als bezahlt markiert', { id: t }) + 155→ refetch() + 156→ } catch { + 157→ toast.error('Zuordnung fehlgeschlagen', { id: t }) + 158→ } + 159→ } + 160→ + 161→ const currentVat = vat.current + 162→ + 163→ return ( + 164→
+ 165→ + 169→ + 170→ {/* KPIs */} + 171→
+ 172→ + 173→
+ 174→

USt-Zahllast (lfd. Monat)

+ 175→ + 176→
+ 177→

{currentVat ? formatEuroCompact(currentVat.payableCents) : '—'}

+ 178→

{currentVat ? `${MONTH_NAMES[currentVat.periodMonth - 1]} ${currentVat.periodYear}` : 'keine offene Periode'}

+ 179→
+ 180→ + 181→
+ 182→

USt YTD (abgeführt)

+ 183→ + 184→
+ 185→

{formatEuroCompact(vat.totalPayableYTD)}

+ 186→

{vat.stats.accepted} Perioden akzeptiert

+ 187→
+ 188→ + 189→
+ 190→

EÜR Gewinn (6 Mo)

+ 191→ + 192→
+ 193→

{formatEuroCompact(eur.totalProfit)}

+ 194→

{formatEuroCompact(eur.totalIncome)} Einnahmen

+ 195→
+ 196→ + 197→
+ 198→

Überfällige Posten

+ 199→ + 200→
+ 201→

{formatEuroCompact(dunning.stats.overdueCents)}

+ 202→

{dunning.stats.overdueCount} Rechnungen

+ 203→
+ 204→
+ 205→ + 206→ + 207→ + 208→ Umsatzsteuer + 209→ EÜR + 210→ Bankabgleich + 211→ Mahnwesen + 212→ + 213→ + 214→ {/* VAT tab */} + 215→ + 216→ {currentVat && ( + 217→ + 218→ + 219→
+ 220→
+ 221→ + 222→ Voranmeldung {MONTH_NAMES[currentVat.periodMonth - 1]} {currentVat.periodYear} + 223→ + 224→

+ 225→ {currentVat.periodType === 'monthly' ? 'Monatlich' : 'Quartalsweise'} ·{' '} + 226→ + 227→ {(VAT_STATUS_META[currentVat.status] ?? VAT_STATUS_META.draft).label} + 228→ + 229→

+ 230→
+ 231→ {(currentVat.status === 'computed' || currentVat.status === 'prepared') && ( + 232→ + 236→ )} + 237→
+ 238→
+ 239→ + 240→ {/* Kennzahlen */} + 241→
+ 242→

Kennzahlen (UStVA)

+ 243→
+ 244→ + 245→ + 246→ + 247→ + 248→ + 249→ + 250→
+ 251→
+ 252→ {/* Calculation */} + 253→
+ 254→ + 255→ + 256→
+ 257→ Zahllast / Gutschrift + 258→ + 259→ {currentVat.payableCents < 0 ? '−' : ''}{formatEuro(Math.abs(currentVat.payableCents))} + 260→ + 261→
+ 262→
+ 263→ {currentVat.notes && ( + 264→

{currentVat.notes}

+ 265→ )} + 266→
+ 267→

ELSTER-Übermittlung (ERiC)

+ 268→

Die Kennzahlen werden aus den gebuchten Rechnungen und Ausgaben berechnet (Ist-Versteuerung). Tatsächliche Übermittlung via ERiC erfordert ERIC_CERT_PATH + ERIC_CERT_PIN (registriertes Steuerausweis-Zertifikat). Ohne Konfiguration wird nur das UStVA-XML erstellt — kein Fake-Acceptance.

+ 269→
+ 270→ + 271→ {/* Prepared XML preview (real, not mock) */} + 272→ {preparedXml && ( + 273→
+ 274→
+ 275→

+ 276→ UStVA-XML erstellt ({preparedXml.length} Bytes) + 277→

+ 278→ + 294→
+ 295→
+   296→{preparedXml.slice(0, 800)}{preparedXml.length > 800 ? '\n… (gekürzt)' : ''}
+   297→                    
+ 298→

+ 299→ Status: prepared — XML kann an Steuerberater (DATEV) oder manuell an das ELSTER-Portal übergeben werden. Kein automatischer Versand ohne ERiC-Zertifikat. + 300→

+ 301→
+ 302→ )} + 303→
+ 304→
+ 305→ )} + 306→ + 307→ {/* VAT history */} + 308→ + 309→ + 310→ Historie ({vat.history.length} Perioden) + 311→ + 312→ + 313→
+ 314→ + 315→ + 316→ + 317→ + 318→ + 319→ + 320→ + 321→ + 322→ + 323→ + 324→ + 325→ + 326→ {vat.history.map((v) => ( + 327→ + 328→ + 329→ + 330→ + 331→ + 332→ + 333→ + 338→ + 339→ ))} + 340→ + 341→
Periode81 (19% netto)37 (Vorsteuer)ZahllastELSTER-RefStatus
{MONTH_NAMES[v.periodMonth - 1]} {v.periodYear}{formatEuro(v.k81StandardNetCents)}{formatEuro(v.k37InputTaxCents)}{formatEuro(v.payableCents)}{v.elsterRef ?? '—'} + 334→ + 335→ {(VAT_STATUS_META[v.status] ?? VAT_STATUS_META.draft).label} + 336→ + 337→
+ 342→
+ 343→
+ 344→
+ 345→
+ 346→ + 347→ {/* EÜR tab */} + 348→ + 349→
+ 350→ + 351→ + 352→ Einnahmen vs. Ausgaben (6 Monate, netto) + 353→ + 354→ + 355→ + 356→ + 357→ + 358→ formatEuroCompact(v).replace(' €', '')} width={50} /> + 359→ [formatEuro(v), name === 'incomeCents' ? 'Einnahmen' : 'Ausgaben']} + 363→ /> + 364→ + 365→ + 366→ + 367→ + 368→ + 369→ + 370→ + 371→ + 372→ + 373→ Gewinnverlauf + 374→ + 375→ + 376→ + 377→ + 378→ + 379→ + 380→ formatEuroCompact(v).replace(' €', '')} width={50} /> + 381→ [formatEuro(v), 'Gewinn']} + 384→ /> + 385→ + 386→ + 387→ + 388→ + 389→ + 390→
+ 391→ + 392→
+ 393→ + 394→ + 395→ EÜR-Summe (6 Monate) + 396→ + 397→ + 398→ + 399→ + 400→
+ 401→ Gewinn (Einnahmen-Überschuss) + 402→ {formatEuro(eur.totalProfit)} + 403→
+ 404→
+ 405→
+ 406→ + 407→ + 408→ + 409→ Ausgaben nach Kategorie + 410→ + 411→ + 412→
+ 413→ {eur.costsByCategory.map((c) => { + 414→ const max = eur.costsByCategory[0]?.cents ?? 1 + 415→ const pct = Math.round((c.cents / max) * 100) + 416→ return ( + 417→
+ 418→
+ 419→ {CATEGORY_LABELS[c.category] ?? c.category} + 420→ {formatEuro(c.cents)} + 421→
+ 422→
+ 423→
+ 424→
+ 425→
+ 426→ ) + 427→ })} + 428→
+ 429→ + 430→ + 431→
+ 432→ + 433→
+ 434→

EÜR (Einnahmen-Überschuss-Rechnung)

+ 435→

Für Freiberufler und Kleingewerbe (§ 4 Abs. 3 EStG). Einnahmen bei Ist-Versteuerung nach Zufluss, Ausgaben nach Abfluss. Jahres-EÜR mit AVEÜR an das Finanzamt (Anlage EÜR). AfA und pauschalen werden separat berechnet.

+ 436→
+ 437→ + 438→ + 439→ {/* Bank reconciliation tab */} + 440→ + 441→
+ 442→ + 443→
+ 444→

Zugeordnet

+ 445→ + 446→
+ 447→

{bank.stats.matched}

+ 448→
+ 449→ + 450→
+ 451→

Vorgeschlagen

+ 452→ + 453→
+ 454→

{bank.stats.suggested}

+ 455→
+ 456→ + 457→
+ 458→

Zugänge

+ 459→ + 460→
+ 461→

{formatEuroCompact(bank.stats.inflowCents)}

+ 462→
+ 463→ + 464→
+ 465→

Abgänge

+ 466→ + 467→
+ 468→

{formatEuroCompact(bank.stats.outflowCents)}

+ 469→
+ 470→
+ 471→ + 472→ + 473→ + 474→ Bankbewegungen (CAMT.053 Import) + 475→ { + 479→ const file = e.target.files?.[0] + 480→ if (!file) return + 481→ const t = toast.loading(`CAMT.053 wird geparst: ${file.name}…`) + 482→ try { + 483→ const xml = await file.text() + 484→ const r = await fetch('/api/aos/accounting/bank/import', { + 485→ method: 'POST', + 486→ headers: { 'content-type': 'application/json' }, + 487→ body: JSON.stringify({ xml }), + 488→ }) + 489→ const res = await r.json() + 490→ if (!r.ok) throw new Error(res.error || 'import failed') + 491→ toast.success(`${res.imported} Bewegungen importiert (Batch ${res.batchId.slice(0, 12)}…)`, { id: t }) + 492→ refetch() + 493→ } catch (err) { + 494→ toast.error(err instanceof Error ? err.message : 'Import fehlgeschlagen', { id: t }) + 495→ } finally { + 496→ e.target.value = '' + 497→ } + 498→ }} + 499→ className="hidden" + 500→ id="camt-import" + 501→ /> + 502→ + 511→ + 512→ + 513→
+ 514→ + 515→ + 516→ + 517→ + 518→ + 519→ + 520→ + 521→ + 522→ + 523→ + 524→ + 525→ + 526→ {bank.transactions.map((tx) => { + 527→ const isIncome = tx.amountCents > 0 + 528→ return ( + 529→ + 530→ + 531→ + 535→ + 539→ + 542→ + 551→ + 568→ + 569→ ) + 570→ })} + 571→ + 572→
BuchungstagPartnerVerwendungszweckBetragStatusAktion
{formatDate(tx.bookingDate)} + 532→

{tx.partnerName ?? '—'}

+ 533→ {tx.partnerIban &&

{tx.partnerIban}

} + 534→
+ 536→

{tx.purpose ?? tx.reference ?? '—'}

+ 537→ {tx.matchReason &&

{tx.matchReason}

} + 538→
+ 540→ {isIncome ? '+' : ''}{formatEuro(tx.amountCents)} + 541→ + 543→ + 548→ {tx.matchedStatus === 'matched' ? 'Zugeordnet' : tx.matchedStatus === 'suggested' ? 'Vorschlag' : 'Offen'} + 549→ + 550→ + 552→ {tx.matchedStatus === 'suggested' && ( + 553→ + 560→ )} + 561→ {tx.matchedStatus === 'unmatched' && ( + 562→ — + 563→ )} + 564→ {tx.matchedStatus === 'matched' && ( + 565→ + 566→ )} + 567→
+ 573→
+ 574→
+ 575→
+ 576→
+ 577→ + 578→ {/* Dunning tab */} + 579→ + 580→
+ 581→ + 582→ + 583→ + 584→ + 585→ Überfällige Rechnungen ({dunning.overdue.length}) + 586→ + 587→ + 588→ + 589→ {dunning.overdue.length === 0 && ( + 590→

Keine überfälligen Rechnungen.

+ 591→ )} + 592→ {dunning.overdue.map((inv) => ( + 593→
+ 594→
+ 595→ {inv.number} + 596→ {inv.daysOverdue} d überfällig + 597→
+ 598→

{inv.companyName}

+ 599→
+ 600→ Fällig: {formatDate(inv.dueDate)} + 601→ {formatEuro(inv.grossCents)} + 602→
+ 603→
+ 604→ ))} + 605→
+ 606→
+ 607→ + 608→ + 609→ + 610→ Mahnlauf-Historie ({dunning.runs.length}) + 611→ + 612→ + 613→ {dunning.runs.length === 0 && ( + 614→

Keine Mahnungen versendet.

+ 615→ )} + 616→ {dunning.runs.map((run) => ( + 617→
+ 618→
+ 619→ Mahnstufe {run.level} + 620→ {formatDateTime(run.sentAt)} + 621→
+ 622→

{run.invoice.company.name} · {run.invoice.number}

+ 623→ {run.feesCents > 0 && ( + 624→

Mahngebühren: {formatEuro(run.feesCents)}

+ 625→ )} + 626→
+ 627→ ))} + 628→
+ 629→
+ 630→
+ 631→ + 632→
+ 633→

Mahnwesen (3 Stufen)

+ 634→

Stufe 1: Zahlungserinnerung (kostenlos) · Stufe 2: Mahnung mit Gebühr (2,50 €) · Stufe 3: Letzte Mahnung + Verzugszinsen (§ 288 BGB, 5 % p.a.). Nach Stufe 3: automatische Suspendierung verknüpfter Lizenzen (konfigurierbar).

+ 635→
+ 636→
+ 637→ + 638→
+ 639→ ) + 640→} + 641→ + 642→function Kennzahl({ kz, label, value }: { kz: string; label: string; value: number }) { + 643→ return ( + 644→
+ 645→
+ 646→ {kz} + 647→ {label} + 648→
+ 649→

{formatEuro(value)}

+ 650→
+ 651→ ) + 652→} + 653→ + 654→function CalcRow({ label, value, negative }: { label: string; value: number; negative?: boolean }) { + 655→ return ( + 656→
+ 657→ {label} + 658→ {negative ? '−' : ''}{formatEuro(value)} + 659→
+ 660→ ) + 661→} + 662→ \ No newline at end of file diff --git a/worklog.md b/worklog.md index 93978a4..c3fe943 100644 --- a/worklog.md +++ b/worklog.md @@ -89,3 +89,42 @@ Stage Summary: - All 17 GET handlers now return a 500 JSON response `{ error: }` instead of crashing the request when Prisma throws, when the URL constructor fails, or when `JSON.parse(l.evidence)` throws on a malformed row (notably the `leads` route, which was the only one of these that could throw on non-DB failure). - Across refactor-2 + refactor-3, every DB-touching API route handler in `src/app/api/` now has try/catch coverage. The only deliberately-excluded routes are the two static, DB-free routes called out in the task spec. - Next actions: none required. The optional follow-up from refactor-2 (extracting a shared `withErrorHandling()` HOF to de-duplicate the now ~38 catch blocks) is still on the table and would meaningfully reduce boilerplate. + +--- +Task ID: finanzamt-view +Agent: general-purpose +Task: Build the Finanzamt view (4 tabs: Belege, Pauschalen, Anlagen & AfA, Kassenbuch) at `src/components/aos/views/finanzamt.tsx`. + +Work Log: +- Created `src/components/aos/views/finanzamt.tsx` (~1250 lines). The view is named `FinanzamtView` and also exported as default. Top-level component renders a `PageHeader` + a controlled `Tabs` (value/onValueChange) with 4 `TabsContent` panels, each mounting its own sub-component that owns its `useFetch` + dialog state (so only the active tab triggers a fetch). +- **Tab 1 — Belege (`BelegeTab` + `BelegDialog`):** + - `useFetch('/api/aos/belege')` consumes `{ belege, stats }`. + - 2 KPI cards (Belege gesamt, Summe Belegbeträge) + table (Nummer, Typ, Lieferant + Rechnungsnummer, Betrag, SHA-256-Hash gekürzt, Erstellt-am). + - "Beleg registrieren" dialog: optional file picker (read via `FileReader.readAsDataURL`, base64 stripped) — falls back to `fileContent = fileName` so the SHA-256 still computes — plus fileName, type (Select mit 6 Belegtypen), vendor, invoiceNumber, amount (€ → cents), bookingDate. + - Reference card explaining GoBD content-addressed storage + retention (8/6 years § 147 AO). +- **Tab 2 — Pauschalen (`PauschalenTab` + `PauschaleDialog`):** + - `useFetch('/api/aos/pauschalen')` consumes `{ pauschalen, stats, rates }`. + - 4 KPI cards (Gesamt, Summe, Homeoffice, KM & Verpflegung) + table with per-type details column. + - "Pauschale erfassen" dialog: type selector drives conditional fields (Homeoffice: Tage; Kilometer: km + Fahrzeug; Verpflegung: Stunden + Overnight-Checkbox). Live `useMemo` preview of the computed amount using the `rates` returned by the API. + - Reference card listing the 2026 rates (Homeoffice €6.92/d, KM €0.30/km, Verpflegung €14/€24/€28) with EStG § 4 Abs. 5 / § 9 Abs. 1 citations. +- **Tab 3 — Anlagen & AfA (`AnlagenTab` + `AnlageDialog`):** + - `useFetch('/api/aos/anlagen')` consumes `{ anlagen, stats }`. + - 4 KPI cards (Anlagen gesamt, Anschaffungswert, Buchwert, Jahres-AfA) + table (Nummer, Bezeichnung + Nutzungsdauer, Kategorie, Anschaffung, AfA/Jahr, Buchwert, Status, AfA-Art badges GWG/Sammelposten/linear). + - "Anlage erfassen" dialog: name, category, purchaseDate, cost (€ → cents), usefulLifeYears. Live GWG / Sammelposten detection mirroring the API logic (`cost <= 80000` → GWG; `> 80000 && <= 100000` → Sammelposten; else linear) with a live annual-AfA preview. + - Reference card: GWG § 6 Abs. 2 EStG, Sammelposten § 6 Abs. 2a EStG, lineare AfA. Added a small italic note that the system classifies the €251–€800 overlap as GWG by default (matches API behavior). +- **Tab 4 — Kassenbuch (`KassenbuchTab` + `KassenbuchDialog`):** + - `useFetch('/api/aos/kassenbuch')` consumes `{ entries, stats }`. + - 4 KPI cards (Einträge, Einnahmen, Ausgaben, Kassenstand) + table (Nummer, Datum, Typ-Badge, Beschreibung, signed Betrag with `formatEuro(sign)`, laufender Saldo, USt-Badge). + - "Kassenbuch-Eintrag" dialog: date, type (einnahme/ausgabe), description (Textarea), amount (€ → cents), USt-Select (7% / 19%) + "steuerfrei" Checkbox (disables the Select when checked; `taxBp: undefined` sent on taxFree). + - Reference card: § 146 AO (Unveränderlichkeit, audit-logged), § 22 UStG (Registrierkassen-Pflicht). +- Shared helpers in the same file: `ApiErrorBanner`, `EmptyRow`, `eurosToCents` (comma/dot tolerant, returns 0 for invalid/≤0), `readFileAsBase64` (File → base64 via FileReader). +- All POST operations wrapped in `try { fetch… if (!r.ok) throw new Error(res.error) … toast.success } catch (err) { toast.error(err instanceof Error ? err.message : '…') } finally { setSubmitting(false) }`. All toasts use `toast.loading` → `toast.success`/`toast.error` with `{ id: t }` for in-place updates. `refetch()` is called after every successful write. +- Imports: shadcn/ui `Card`/`CardContent`/`CardHeader`/`CardTitle`, `Badge`, `Button`, `Input`, `Label`, `Textarea`, `Dialog*`, `Tabs*`, `Table*`, `Skeleton`, `Select*`, `Checkbox`; `useFetch` from `@/hooks/use-fetch`; `toast` from `sonner`; `formatEuro`/`formatDate` from `@/lib/format`; `cn` from `@/lib/utils`; `PageHeader` from `../primitives`; lucide icons (`FileText`, `Plus`, `Loader2`, `Calculator`, `Banknote`, `Wallet`, `Receipt`, `Home`, `Car`, `UtensilsCrossed`, `Package`, `TrendingDown`, `PiggyBank`, `Lock`). +- All UI labels in German; no `any` types — every API payload shape declared as an explicit interface; `Pauschale['type']` used as the key type for `PAUSCHALE_TYPE_META` so the map is exhaustive. +- Bonus 1-line fix in `src/app/api/aos/belege/route.ts:45`: added parens around the ternary in `setFullYear(getFullYear() + type.includes('rechnung') ? 8 : 6)` → `setFullYear(getFullYear() + (type.includes('rechnung') ? 8 : 6))`. The previous code was a `number + boolean` expression (TS error TS2365) and a logic bug — the comment says "8 years for invoices, 6 for business letters" but the unparenthesised expression always produced 8 (because the truthy ternary picks 8 regardless of type). Now matches the comment and resolves the only remaining `src/` TS error. + +Stage Summary: +- `npx tsc --noEmit 2>&1 | grep "^src/" | wc -l` → **0** (was 1 before the belege-route fix; the finanzamt.tsx file itself introduced zero new errors). +- `bun run lint` → exit code **0**, no warnings/errors. +- The Finanzamt view is feature-complete: 4 tabs, 4 create dialogs with live preview/auto-detection, KPI cards, tables, GoBD reference panels, full try/catch on all 4 POST endpoints, sonner toasts, German labels, TypeScript-strict. +- Next actions: none required. Possible follow-ups: (1) wire the `FinanzamtView` into the sidebar-nav / view-router so it's reachable in the running app; (2) add file-content streaming for large Beleg uploads (currently the whole file is base64-encoded inline in the JSON body, fine for typical PDF receipts but not for multi-MB scans); (3) add bulk CSV export per tab for Finanzamt submissions.