import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { api } from "../api";
import { useSettings } from "../settings";
import IconChip from "../components/IconChip";
import Money from "../components/Money";
import { formatMoney, formatAmount } from "../format";
import { tokenFarbe } from "./palette";
import Donut from "./charts/Donut";
import Line from "./charts/Line";
function useData(loader) {
const [data, setData] = useState(null);
useEffect(() => {
let ok = true;
loader().then((d) => ok && setData(d)).catch(() => ok && setData({ __error: true }));
return () => { ok = false; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return data;
}
function Loading() {
const { t } = useTranslation();
return
{t("common.loading")}
;
}
function shortDate(iso) {
const [, m, d] = iso.split("-");
return `${d}.${m}.`;
}
function AccountBalances() {
const { t } = useTranslation();
const { baseCurrency } = useSettings();
const data = useData(() => api.cardBalances());
if (!data) return ;
if (!data.accounts?.length) return {t("accounts.empty")}
;
return (
{data.accounts.map((a) => (
{a.name}
))}
{t("cards.total")}
{formatMoney(data.total, data.currency)}
);
}
function BudgetDonut({ props }) {
const { t } = useTranslation();
const groupBy = props?.group_by || "group";
const data = useData(() => api.cardBudgetDonut(groupBy));
if (!data) return ;
if (!data.segments?.length) return {t("cards.noBudget")}
;
const label = data.mode === "planned" ? t("cards.planned") : t("cards.spent");
return formatMoney(v, data.currency)} />;
}
function NetWorth() {
const { t } = useTranslation();
const data = useData(() => api.cardNetWorth(90));
if (!data) return ;
const labels = (data.labels || []).map(shortDate);
const serien = [{ name: t("cards.assets"), color: tokenFarbe("--accent", "#3f51b5"), werte: data.values || [] }];
return (
formatMoney(v, data.currency)}
achsenFmt={(v) => formatAmount(v, data.currency)} />
);
}
function BudgetProgress({ props }) {
const { t } = useTranslation();
const groupBy = props?.group_by || "group";
const data = useData(() => api.cardBudgetProgress(groupBy));
if (!data) return ;
if (!data.rows?.length) return {t("cards.noBudget")}
;
return (
{data.rows.map((r) => {
const pct = r.planned > 0 ? Math.min(r.actual / r.planned, 1) * 100 : r.actual > 0 ? 100 : 0;
return (
●
{r.name}
{formatMoney(r.actual, data.currency)}
/ {formatMoney(r.planned, data.currency)}
);
})}
);
}
function IncomeExpense() {
const { t } = useTranslation();
const data = useData(() => api.cardIncomeExpense());
if (!data) return ;
return (
{t("cards.income")}
{formatMoney(data.income, data.currency)}
{t("cards.expense")}
{formatMoney(data.expense, data.currency)}
{t("cards.net")}
{formatMoney(data.net, data.currency)}
);
}
function RecentTransactions() {
const { t } = useTranslation();
const { formatDate } = useSettings();
const data = useData(() => api.cardRecent(8));
if (!data) return ;
if (!data.transactions?.length) return {t("transactions.empty")}
;
return (
{data.transactions.map((tx) => (
{tx.title}
{formatDate(tx.date)}
))}
);
}
function ExpenseShare({ props }) {
const { t } = useTranslation();
const groupBy = props?.group_by || "category";
const data = useData(() => api.cardExpenseShare(groupBy));
if (!data) return ;
if (!data.rows?.length) return {t("cards.noExpenses")}
;
return (
{data.rows.map((r) => {
// Anteil aus den Rohwerten – gerundete Prozente ergäben in der Summe
// sonst nicht 100 %.
const pct = data.total > 0 ? (r.amount / data.total) * 100 : 0;
return (
●
{r.name}
{pct.toFixed(1).replace(".", ",")} %
{formatMoney(r.amount, data.currency)}
);
})}
{t("cards.total")}
{formatMoney(data.total, data.currency)}
);
}
// Aufschlüsselungs-Option (Gruppen vs. Kategorien) – von Donut und Fortschritt geteilt.
const AUFSCHLUESSELUNG = {
key: "group_by", labelKey: "cards.breakdown", def: "group",
choices: [
{ v: "group", labelKey: "cards.byGroup" },
{ v: "category", labelKey: "cards.byCategory" },
],
};
// Registry: Kartentyp -> { Titel-Schlüssel, Komponente, Standardgröße/Mindestgröße,
// optional `options` (im Anordnen-Modus einstellbare Karten-Optionen) }.
export const KARTEN = {
"account-balances": { titleKey: "cards.balances", Comp: AccountBalances, w: 4, h: 5, minW: 3, minH: 3 },
"budget-donut": { titleKey: "cards.budgetDonut", Comp: BudgetDonut, w: 4, h: 5, minW: 3, minH: 4, options: [AUFSCHLUESSELUNG] },
"net-worth": { titleKey: "cards.netWorth", Comp: NetWorth, w: 8, h: 4, minW: 4, minH: 3 },
"budget-progress": { titleKey: "cards.budgetProgress", Comp: BudgetProgress, w: 4, h: 3, minW: 3, minH: 2, options: [AUFSCHLUESSELUNG] },
"expense-share": { titleKey: "cards.expenseShare", Comp: ExpenseShare, w: 4, h: 5, minW: 3, minH: 3,
options: [{ ...AUFSCHLUESSELUNG, def: "category" }] },
"income-expense": { titleKey: "cards.incomeExpense", Comp: IncomeExpense, w: 4, h: 2, minW: 2, minH: 2 },
"recent-transactions": { titleKey: "cards.recent", Comp: RecentTransactions, w: 4, h: 4, minW: 3, minH: 3 },
};
export const KARTEN_TYPEN = Object.keys(KARTEN);