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→