feat: Oberflaeche fuer die Schnellerfassung
- "Mein Konto": Geraetemarken anlegen und widerrufen. Der Klartext ist nur einmal zu sehen, daneben steht eine Bauanleitung fuer den Kurzbefehl mit kopierfertiger Adresse und Kopfzeile. Ein fertiger Download geht nicht: seit iOS 15 muessen Kurzbefehl-Dateien signiert sein, und signieren kann nur ein Mac mit Apple-Konto. - Neue Seite "Eingang": was unterwegs erfasst wurde, mit Kategorie-Auswahl und Verwerfen. - Die Import-Pruefliste unterscheidet jetzt, WARUM eine Zeile uebersprungen wird: "schon erfasst" mit Erklaerung statt nur "Duplikat". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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 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>
|
||||
</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">
|
||||
{formatDate(item.date)} · {konto?.name || "—"}
|
||||
{item.source === "shortcut" && ` · ${t("inbox.viaShortcut")}`}
|
||||
</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)}
|
||||
/>
|
||||
<button className="btn sm ghost" title={t("inbox.discard")}
|
||||
onClick={() => verwerfen(item)}>
|
||||
<Icon name="trash" size={15} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user