Am Anfang einer Betreuung weiss niemand, welche Vertraege es gibt. Die Antwort steht aber im Kontoauszug: Wer regelmaessig abbucht, hat einen Vertrag. Neu unter Vermoegen & Kasse wird aus hunderten Buchungszeilen eine Arbeitsliste. Aufbau: - Die Erkennung liegt als reine Funktionen in src/lib/finanzen/wiederkehrend.ts, ohne Datenbank. Nur so laesst sich jede Einordnung einzeln nachrechnen. - Der Datenzugriff steht getrennt in wiederkehrend-laden.ts. - Ein kleines Modell haelt fest, was bewusst KEIN Vertrag ist. Es haengt am normalisierten Empfaengernamen, nicht an Buchungen - nur so ueberlebt die Entscheidung den naechsten Import. Zwei Haltungen liegen der Erkennung zugrunde: Im Zweifel getrennt lassen. Zusammengefasst wird nur das Unstrittige: Gross- und Kleinschreibung, Satzzeichen, Umlaute, Rechtsform am Ende. Keine Aehnlichkeitssuche - "Stadtwerke Bamberg" und "Stadtwerke Bayreuth" trennen drei Zeichen und zwei Rechnungssteller. Zwei Zeilen zu viel sind harmlos; eine falsche Gruppe verbirgt einen Vertrag. Nichts behaupten, was nicht belegt ist. Statt "monatlich" steht dort "6 Zahlungen, etwa monatlich". Schwankt der Betrag, wird die Spanne genannt. Ohne erkennbaren Abstand wird nicht hochgerechnet, sondern gezeigt, was geflossen ist. Dazu eine zweite Liste fuer einzelne groessere Zahlungen: Die Gruppierung braucht zwei Zahlungen, ein Jahresbeitrag erscheint bei einem importierten Jahr aber nur einmal - und ausgerechnet Versicherungen zahlt man oft jaehrlich. Geprueft: 37 Einheitstests, die Haelfte davon in der Gegenrichtung, plus vier End-to-End-Tests auf beiden Geraeteprofilen. Der Browsertest hat zwei Fehler gefunden, die Typpruefung und Einheitstests durchgelassen haben - darunter ein verlorener Backslash, der die Vorbelegung des Beitrags stillschweigend verschluckte. Nebenbei: .claude/ ins .gitignore. Dort legen parallel arbeitende Agenten ihre Worktrees ab; ein "git add -A" haette einen kompletten zweiten Checkout eingecheckt. Refs #3
134 lines
5.3 KiB
TypeScript
134 lines
5.3 KiB
TypeScript
import Link from 'next/link'
|
||
import { Repeat, ReceiptText } from 'lucide-react'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Leerzustand } from '@/components/seiten-kopf'
|
||
import { Kachel, KachelZeile } from '@/components/dashboard/kachel'
|
||
import type { BelegeDaten } from '@/lib/dashboard/typen'
|
||
import { formatCent, formatDatum } from '@/lib/utils'
|
||
|
||
/**
|
||
* Belege und Buchungen, die noch nicht zusammengefunden haben.
|
||
*
|
||
* Fuer die Rechnungslegung gilt Belegzwang: zu jeder Buchung gehoert ein
|
||
* Beleg, und wo keiner existiert, gehoert eine Begruendung dazu. Diese
|
||
* Kachel zeigt beide Luecken - Belege ohne Buchung und Buchungen ohne
|
||
* Beleg.
|
||
*/
|
||
export function BelegeKachel({ daten }: { daten: BelegeDaten | null }) {
|
||
if (!daten) return null
|
||
|
||
const offen =
|
||
daten.belegeOhneBuchungGesamt +
|
||
daten.buchungenOhneBelegGesamt +
|
||
daten.wiederkehrendOhneVertrag
|
||
|
||
return (
|
||
<Kachel
|
||
titel="Unverbuchte Belege"
|
||
icon={ReceiptText}
|
||
zaehler={offen}
|
||
hinweis={
|
||
offen > 0
|
||
? 'Zu jeder Buchung gehört ein Beleg – oder eine Begründung, warum keiner existiert.'
|
||
: undefined
|
||
}
|
||
aktion={
|
||
<Button asChild variant="outline">
|
||
<Link href="/finanzen/buchungen?filter=ohne-beleg">Kassenbuch öffnen</Link>
|
||
</Button>
|
||
}
|
||
>
|
||
{offen === 0 ? (
|
||
<Leerzustand
|
||
icon={ReceiptText}
|
||
titel="Belege und Buchungen passen zusammen."
|
||
beschreibung="Jede erfasste Buchung hat einen Beleg oder eine Begründung, und keine Rechnung wartet auf ihre Buchung."
|
||
className="py-8"
|
||
/>
|
||
) : (
|
||
<div className="space-y-5">
|
||
{daten.belegeOhneBuchungGesamt > 0 && (
|
||
<section aria-labelledby="belege-ohne-buchung">
|
||
<h3
|
||
id="belege-ohne-buchung"
|
||
className="mb-1 text-sm font-semibold uppercase tracking-wide text-muted-foreground"
|
||
>
|
||
Rechnungen und Quittungen ohne Buchung ({daten.belegeOhneBuchungGesamt})
|
||
</h3>
|
||
<ul className="divide-y">
|
||
{daten.belegeOhneBuchung.map((beleg) => (
|
||
<li key={beleg.id}>
|
||
<KachelZeile href={`/finanzen/buchungen/neu?beleg=${beleg.id}`}>
|
||
<span className="min-w-0 flex-1">
|
||
<span className="block font-medium">{beleg.titel}</span>
|
||
<span className="block text-sm text-muted-foreground">
|
||
{beleg.eingangAm
|
||
? `Eingang ${formatDatum(beleg.eingangAm)}`
|
||
: 'ohne Eingangsdatum'}{' '}
|
||
· Buchung anlegen
|
||
</span>
|
||
</span>
|
||
</KachelZeile>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
)}
|
||
|
||
{daten.wiederkehrendOhneVertrag > 0 && (
|
||
<section aria-labelledby="wiederkehrend-ohne-vertrag">
|
||
<h3
|
||
id="wiederkehrend-ohne-vertrag"
|
||
className="mb-1 text-sm font-semibold uppercase tracking-wide text-muted-foreground"
|
||
>
|
||
Regelmäßige Abbuchungen ohne Vertrag ({daten.wiederkehrendOhneVertrag})
|
||
</h3>
|
||
<KachelZeile href="/finanzen/wiederkehrend">
|
||
<span className="min-w-0 flex-1">
|
||
<span className="flex items-center gap-2 font-medium">
|
||
<Repeat className="size-4 shrink-0" aria-hidden="true" />
|
||
{daten.wiederkehrendOhneVertrag} Empfänger ansehen
|
||
</span>
|
||
<span className="block text-sm text-muted-foreground">
|
||
Wer regelmäßig abbucht, hat einen Vertrag — und der fehlt dann im
|
||
Vermögensverzeichnis.
|
||
</span>
|
||
</span>
|
||
</KachelZeile>
|
||
</section>
|
||
)}
|
||
|
||
{daten.buchungenOhneBelegGesamt > 0 && (
|
||
<section aria-labelledby="buchungen-ohne-beleg">
|
||
<h3
|
||
id="buchungen-ohne-beleg"
|
||
className="mb-1 text-sm font-semibold uppercase tracking-wide text-muted-foreground"
|
||
>
|
||
Buchungen ohne Beleg ({daten.buchungenOhneBelegGesamt})
|
||
</h3>
|
||
<ul className="divide-y">
|
||
{daten.buchungenOhneBeleg.map((buchung) => (
|
||
<li key={buchung.id}>
|
||
<KachelZeile href={`/finanzen/buchungen/${buchung.id}`}>
|
||
<span className="min-w-0 flex-1">
|
||
<span className="block font-medium">
|
||
{buchung.richtung === 'AUSGABE' ? '−' : '+'}
|
||
{formatCent(buchung.betragCent)}
|
||
{buchung.gegenstelle ? ` · ${buchung.gegenstelle}` : ''}
|
||
</span>
|
||
<span className="block text-sm text-muted-foreground">
|
||
{formatDatum(buchung.datum)} · Beleg zuordnen oder begründen
|
||
</span>
|
||
</span>
|
||
</KachelZeile>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
)}
|
||
</div>
|
||
)}
|
||
</Kachel>
|
||
)
|
||
}
|