/** Auswertungen: Abos, Kategorien, Jahresvergleich und Export. */ import { useState } from "react"; import { AlertTriangle, Download, Repeat } from "lucide-react"; import { CategoryDonut } from "@/components/charts/CategoryDonut"; import { ChartCard, StatTile } from "@/components/charts/ChartFrame"; import { YearComparisonChart } from "@/components/charts/YearComparisonChart"; import { PageHeader } from "@/components/layout/AppLayout"; import { Button } from "@/components/ui/Button"; import { Badge, EmptyState, Skeleton } from "@/components/ui/Feedback"; import { Select } from "@/components/ui/Field"; import { useMonthStartDay } from "@/hooks/useAppSettings"; import { useCategoryLookup } from "@/hooks/useCategoryLookup"; import { exportUrl, useCategoryReport, useSubscriptions, useYearComparison, } from "@/hooks/useReports"; import { cn } from "@/lib/cn"; import { formatDate, formatMoney, toNumber } from "@/lib/format"; import { periodBounds, periodKey, periodLabel } from "@/lib/period"; import { describeRRule } from "@/lib/rrule"; import type { Subscription } from "@/types/api"; type Reiter = "subscriptions" | "categories" | "year" | "export"; const REITER: { id: Reiter; label: string }[] = [ { id: "subscriptions", label: "Laufende Kosten" }, { id: "categories", label: "Kategorien" }, { id: "year", label: "Jahresvergleich" }, { id: "export", label: "Export" }, ]; export function ReportsPage() { const [reiter, setReiter] = useState("subscriptions"); return ( <>
{REITER.map((eintrag) => ( ))}
{reiter === "subscriptions" && } {reiter === "categories" && } {reiter === "year" && } {reiter === "export" && } ); } /* --- Abos ----------------------------------------------------------------- */ type Sortierung = "annual" | "monthly" | "title" | "notice"; function SubscriptionsTab() { const [sortierung, setSortierung] = useState("annual"); const { data, isLoading } = useSubscriptions(); const kategorieName = useCategoryLookup(); if (isLoading || !data) return ; if (data.entries.length === 0) { return ( ); } const sortiert = [...data.entries].sort((links, rechts) => { switch (sortierung) { case "monthly": return toNumber(rechts.monthly_cost) - toNumber(links.monthly_cost); case "title": return links.title.localeCompare(rechts.title, "de"); case "notice": return (links.days_until_notice ?? 99999) - (rechts.days_until_notice ?? 99999); default: return toNumber(rechts.annual_cost) - toNumber(links.annual_cost); } }); return (
!eintrag.is_installment).length)} hint={`zuzüglich ${data.entries.filter((eintrag) => eintrag.is_installment).length} Ratenzahlungen`} />
{data.upcoming_deadlines.length > 0 && (

Kündigungsfristen der nächsten 60 Tage

    {data.upcoming_deadlines.map((eintrag) => (
  • {eintrag.title} kündbar bis {formatDate(eintrag.contract_term?.notice_deadline)} noch {eintrag.days_until_notice} Tage
  • ))}
)} setSortierung(ereignis.target.value as Sortierung)} > } >
{sortiert.map((eintrag) => ( ))}
Laufende Posten mit Jahreskosten
Posten Rhythmus pro Monat pro Jahr
); } function SubscriptionRow({ entry, categoryName, }: { entry: Subscription; categoryName: string; }) { return (
{entry.title} {entry.is_installment && Raten} {entry.is_cancelled && Gekündigt} {entry.days_until_notice !== null && entry.days_until_notice <= 60 && ( Frist in {entry.days_until_notice} Tagen )}

{entry.merchant_name ? `${entry.merchant_name} · ` : ""} {categoryName}

{describeRRule(entry.rrule)} {formatMoney(entry.monthly_cost)} {formatMoney(entry.annual_cost)} ); } /* --- Kategorien ----------------------------------------------------------- */ function CategoriesTab() { const [zeitraum, setZeitraum] = useState<"month" | "year">("month"); const startDay = useMonthStartDay(); const heute = new Date(); const laufend = periodKey(startDay); const grenzen = periodBounds(startDay, laufend); const von = zeitraum === "month" ? grenzen.start : `${heute.getFullYear()}-01-01`; const bis = zeitraum === "month" ? grenzen.end : `${heute.getFullYear()}-12-31`; const { data, isLoading } = useCategoryReport(von, bis); if (isLoading || !data) return ; return (
{( [ ["month", periodLabel(startDay, laufend)], ["year", String(heute.getFullYear())], ] as const ).map(([wert, beschriftung]) => ( ))}
); } /* --- Jahresvergleich ------------------------------------------------------ */ function YearTab() { const [jahr, setJahr] = useState(() => new Date().getFullYear()); const { data, isLoading } = useYearComparison(jahr); return (
{isLoading || !data ? : }
); } /* --- Export --------------------------------------------------------------- */ function ExportTab() { const startDay = useMonthStartDay(); const [gewaehlt, setGewaehlt] = useState(null); const monat = gewaehlt ?? periodKey(startDay); const jahr = new Date().getFullYear(); return (
setGewaehlt(`${ereignis.target.value}-01`)} className="mb-3 w-full rounded-lg border border-line bg-raised px-3 py-2 text-sm text-ink" /> } links={[ { label: "CSV", href: exportUrl("month", "csv", { month: monat }) }, { label: "Excel", href: exportUrl("month", "xlsx", { month: monat }) }, ]} />
); } function ExportCard({ title, description, links, extra, }: { title: string; description: string; links: { label: string; href: string }[]; extra?: React.ReactNode; }) { return (

{title}

{description}

{extra}
{links.map((verweis) => ( {verweis.label} ))}
); }