Files
certain/web/src/pages/Eingang.jsx
T
GuidoandClaude Opus 5 d8995116cb fix: Eingangsliste – Name bekommt Platz, Auswahl nicht mehr die ganze Zeile
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>
2026-08-26 22:53:48 +02:00

156 lines
5.6 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}