Die Kategorie-Auswahl traegt "width: 100%". In einer Flex-Zeile heisst das "ich haette gern die ganze Zeile", also blieb fuer den Namen daneben nur der Rest – Buchungstexte brachen mitten im Wort um, waehrend das Auswahlfeld uebertrieben breit dastand. Jetzt hat die Auswahl in einer Listenzeile eine eigene Breite, die nur noch schrumpfen darf, der Name nimmt den Rest, und die Betraege stehen in einer Spalte statt ausgefranst. Auf dem Telefon rutschen Auswahl und Knoepfe unter den Namen statt ihn zusammenzuquetschen; Datum und Kontoname bleiben dabei je fuer sich unzertrennlich, damit "Revolut CHF" nicht umbricht – die Waehrung im Namen unterscheidet die beiden Revolut-Konten. Wirkt genauso auf die Seite "Ohne Kategorie", die dieselbe Zeile benutzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
5.6 KiB
React
156 lines
5.6 KiB
React
import { useEffect, useMemo, useState } from "react";
|
||
import { useTranslation } from "react-i18next";
|
||
|
||
import { api } from "../api";
|
||
import CategorySelect from "../components/CategorySelect";
|
||
import Icon from "../components/Icon";
|
||
import IconChip from "../components/IconChip";
|
||
import Money from "../components/Money";
|
||
import { useToast } from "../toast";
|
||
import { useConfirm } from "../confirm";
|
||
import { useSettings } from "../settings";
|
||
import { gruppenFuer } from "../kategorien";
|
||
import { formatDate } from "../format";
|
||
|
||
// Was unterwegs erfasst wurde – per Kurzbefehl vom Telefon. Die Einträge bleiben
|
||
// hier stehen, auch nachdem sie gebucht sind: Der spätere Kontoauszug gleicht
|
||
// gegen sie ab, damit dieselbe Zahlung nicht ein zweites Mal in den Saldo läuft.
|
||
export default function Eingang() {
|
||
const { t } = useTranslation();
|
||
const { toast } = useToast();
|
||
const confirm = useConfirm();
|
||
const { baseCurrency } = useSettings();
|
||
|
||
const [rows, setRows] = useState(null);
|
||
const [accounts, setAccounts] = useState([]);
|
||
const [groups, setGroups] = useState([]);
|
||
|
||
const expenseGroups = useMemo(() => gruppenFuer(groups, "expense"), [groups]);
|
||
const incomeGroups = useMemo(() => gruppenFuer(groups, "income"), [groups]);
|
||
const accountById = useMemo(
|
||
() => Object.fromEntries(accounts.map((a) => [a.id, a])), [accounts]);
|
||
|
||
async function laden() {
|
||
try {
|
||
const [items, acc, cat] = await Promise.all([
|
||
api.listInbox(), api.listAccounts(), api.listCategories(),
|
||
]);
|
||
setRows(items);
|
||
setAccounts(acc);
|
||
setGroups(cat);
|
||
} catch (e) {
|
||
toast(e.message, "error");
|
||
}
|
||
}
|
||
useEffect(() => { laden(); }, []);
|
||
|
||
async function zuordnen(item, categoryId) {
|
||
if (categoryId == null) return;
|
||
try {
|
||
await api.setInboxCategory(item.id, categoryId);
|
||
setRows((r) => r.map((x) => (x.id === item.id ? { ...x, category_id: categoryId } : x)));
|
||
} catch (e) {
|
||
toast(e.message, "error");
|
||
}
|
||
}
|
||
|
||
async function buchen(item) {
|
||
try {
|
||
await api.bookInboxItem(item.id);
|
||
laden();
|
||
} catch (e) {
|
||
toast(e.message, "error");
|
||
}
|
||
}
|
||
|
||
async function alleBuchen() {
|
||
try {
|
||
const r = await api.bookAllInbox();
|
||
toast(t("inbox.bookedN", { n: r.gebucht }));
|
||
laden();
|
||
} catch (e) {
|
||
toast(e.message, "error");
|
||
}
|
||
}
|
||
|
||
async function verwerfen(item) {
|
||
if (!(await confirm({
|
||
title: t("inbox.discard"),
|
||
message: t("inbox.discardConfirm"),
|
||
danger: true,
|
||
confirmLabel: t("inbox.discard"),
|
||
}))) return;
|
||
try {
|
||
await api.discardInboxItem(item.id);
|
||
laden();
|
||
} catch (e) {
|
||
toast(e.message, "error");
|
||
}
|
||
}
|
||
|
||
if (rows === null) return <div className="card"><div className="empty-state">…</div></div>;
|
||
|
||
return (
|
||
<div className="page">
|
||
<div className="page-head">
|
||
<h1>{t("inbox.title")}</h1>
|
||
<p className="muted">{t("inbox.intro")}</p>
|
||
{rows.some((r) => r.status === "pending" && r.account_id) && (
|
||
<button className="btn primary" onClick={alleBuchen}>
|
||
<Icon name="check" size={15} /> {t("inbox.bookAll")}
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{rows.length === 0 ? (
|
||
<div className="card"><div className="empty-state">{t("inbox.empty")}</div></div>
|
||
) : (
|
||
<div className="card">
|
||
<div className="account-list">
|
||
{rows.map((item) => {
|
||
const konto = accountById[item.account_id];
|
||
const ausgabe = item.amount_cents < 0;
|
||
return (
|
||
<div className="account-row" key={item.id}>
|
||
<IconChip name={konto?.icon || "wallet"} color={konto?.color || "#6b7480"} size={26} />
|
||
<div className="named-text grow">
|
||
<div><strong>{item.payee || t("inbox.noText")}</strong></div>
|
||
<div className="named-sub">
|
||
{/* Jede Angabe für sich unzertrennlich: Auf dem Telefon
|
||
brach sonst „Revolut CHF" um oder wurde gekürzt – und
|
||
genau die Währung im Namen unterscheidet die Konten. */}
|
||
<span className="nowrap">{formatDate(item.date)}</span>
|
||
{" · "}
|
||
<span className="nowrap">{konto?.name || "—"}</span>
|
||
{item.source === "shortcut" && ` · ${t("inbox.viaShortcut")}`}
|
||
{item.source === "mail" && ` · ${t("inbox.viaMail")}`}
|
||
{item.status === "unclear" && ` · ${t("inbox.unclear")}`}
|
||
{item.status === "booked" && ` · ${t("inbox.booked")}`}
|
||
</div>
|
||
</div>
|
||
<Money cents={Math.abs(item.amount_cents)} currency={item.currency}
|
||
sign={ausgabe ? "neg" : "pos"} baseCurrency={baseCurrency} />
|
||
<CategorySelect
|
||
groups={ausgabe ? expenseGroups : incomeGroups}
|
||
value={item.category_id}
|
||
onChange={(id) => zuordnen(item, id)}
|
||
/>
|
||
{item.status !== "booked" && item.account_id && (
|
||
<button className="btn sm primary" onClick={() => buchen(item)}>
|
||
<Icon name="check" size={14} /> {t("inbox.book")}
|
||
</button>
|
||
)}
|
||
<button className="btn sm ghost" title={t("inbox.discard")}
|
||
onClick={() => verwerfen(item)}>
|
||
<Icon name="trash" size={15} />
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|