diff --git a/web/src/components/UebertragDialog.jsx b/web/src/components/UebertragDialog.jsx new file mode 100644 index 0000000..c5b91c9 --- /dev/null +++ b/web/src/components/UebertragDialog.jsx @@ -0,0 +1,99 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { api } from "../api"; +import AccountSelect from "./AccountSelect"; +import Modal from "./Modal"; +import Money from "./Money"; +import { useToast } from "../toast"; +import { useConfirm } from "../confirm"; +import { formatDate } from "../format"; + +// Etwas als Kontoübertrag buchen. Zwei Aufrufer – die Arbeitsliste und die +// Buchungsliste –, deshalb eine Komponente: Die Nachfrage bei einem +// geschätzten Betrag ist zu leicht falsch nachgebaut. +// +// `vorgang` beschreibt, was umgewidmet wird: +// { art: "vorschlag" | "buchung", id, kontoId, betrag (vorzeichenbehaftet), +// waehrung, text, datum } +export default function UebertragDialog({ + vorgang, accounts, accountById, baseCurrency, onClose, onFertig, +}) { + const { t } = useTranslation(); + const { toast } = useToast(); + const confirm = useConfirm(); + const [ziel, setZiel] = useState(""); + const [busy, setBusy] = useState(false); + + async function buchen() { + if (!ziel) return; + const senden = (trotzdem) => (vorgang.art === "vorschlag" + ? api.bookInboxAsTransfer(vorgang.id, Number(ziel), trotzdem) + : api.makeTransactionTransfer(vorgang.id, Number(ziel), trotzdem)); + setBusy(true); + try { + try { + await senden(false); + } catch (e) { + // 409 heisst nachfragen: Der Betrag der Gegenseite ist geschätzt und + // das Zielkonto meldet den Zugang selbst. Die Begründung kommt vom + // Server – dort steht das Wissen, nicht hier. + if (e.status !== 409) { toast(e.message, "error"); return; } + if (!(await confirm({ + title: t("inbox.transferTitle"), + message: e.message, + confirmLabel: t("inbox.asTransfer"), + }))) return; + await senden(true); + } + toast(t("inbox.transferBooked")); + onClose(); + await onFertig(); + } catch (e) { + toast(e.message, "error"); + } finally { + setBusy(false); + } + } + + return ( + + + + + } + > +

{t("inbox.transferIntro")}

+
+
+
{vorgang.text || t("inbox.noText")}
+
+ {formatDate(vorgang.datum)} · {accountById[vorgang.kontoId]?.name || "—"} +
+
+ +
+ +
+ ); +} diff --git a/web/src/pages/Eingang.jsx b/web/src/pages/Eingang.jsx index 96f8696..19dbf3c 100644 --- a/web/src/pages/Eingang.jsx +++ b/web/src/pages/Eingang.jsx @@ -2,12 +2,11 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { api } from "../api"; -import AccountSelect from "../components/AccountSelect"; import CategorySelect from "../components/CategorySelect"; import Icon from "../components/Icon"; import IconChip from "../components/IconChip"; -import Modal from "../components/Modal"; import Money from "../components/Money"; +import UebertragDialog from "../components/UebertragDialog"; import Uebertragsvorschlaege from "../components/Uebertragsvorschlaege"; import { useToast } from "../toast"; import { useConfirm } from "../confirm"; @@ -185,38 +184,9 @@ export default function Eingang() { waehrung: accountById[aufgabe.tx.account_id]?.currency, text: aufgabe.tx.payee || aufgabe.tx.description, datum: aufgabe.tx.date, }; - setUebertrag({ ...s, ziel: "" }); + setUebertrag(s); } - async function uebertragBuchen() { - const { art, id, ziel } = uebertrag; - if (!ziel) return; - const senden = (trotzdem) => (art === "vorschlag" - ? api.bookInboxAsTransfer(id, Number(ziel), trotzdem) - : api.makeTransactionTransfer(id, Number(ziel), trotzdem)); - try { - await senden(false); - } catch (e) { - // 409 heisst nachfragen: Der Betrag der Gegenseite ist geschätzt und das - // Zielkonto meldet den Zugang selbst. Die Begründung kommt vom Server – - // dort steht das Wissen, nicht hier. - if (e.status !== 409) { toast(e.message, "error"); return; } - if (!(await confirm({ - title: t("inbox.transferTitle"), - message: e.message, - confirmLabel: t("inbox.asTransfer"), - }))) return; - try { - await senden(true); - } catch (zweiter) { - toast(zweiter.message, "error"); - return; - } - } - setUebertrag(null); - toast(t("inbox.transferBooked")); - await laden(); - } if (inbox === null) { return
…
; @@ -277,49 +247,14 @@ export default function Eingang() { )} {uebertrag && ( - setUebertrag(null)} - footer={ - <> - - - - } - > -

{t("inbox.transferIntro")}

-
-
-
{uebertrag.text || t("inbox.noText")}
-
- {formatDate(uebertrag.datum)} ·{" "} - {accountById[uebertrag.kontoId]?.name || "—"} -
-
- -
- -
+ onFertig={laden} + /> )} ); diff --git a/web/src/pages/Transactions.jsx b/web/src/pages/Transactions.jsx index 135fe2b..ae9460e 100644 --- a/web/src/pages/Transactions.jsx +++ b/web/src/pages/Transactions.jsx @@ -10,6 +10,7 @@ import Money from "../components/Money"; import Modal from "../components/Modal"; import AccountSelect from "../components/AccountSelect"; import CategorySelect from "../components/CategorySelect"; +import UebertragDialog from "../components/UebertragDialog"; import DataTable from "../components/DataTable"; import { gruppenFuer } from "../kategorien"; import { centsToInput, parseAmountToCents, todayIso } from "../format"; @@ -43,6 +44,8 @@ export default function Transactions() { const [txns, setTxns] = useState([]); // Je Buchungs-ID die Gegenseite, falls sie Teil eines Übertrags ist. const [links, setLinks] = useState({}); + // Buchung, die gerade zum Übertrag gemacht wird. + const [uebertrag, setUebertrag] = useState(null); const [accounts, setAccounts] = useState([]); const [groups, setGroups] = useState([]); const [filter, setFilter] = useState({ account_id: "", type: "" }); @@ -243,13 +246,13 @@ export default function Transactions() { const txnColumns = useMemo(() => [ { - key: "date", header: t("transactions.date"), width: 112, + key: "date", header: t("transactions.date"), width: 100, sortValue: (tx) => tx.date, render: (tx) => {formatDate(tx.date)}, }, { // Wofür die Zahlung war – danach sucht man in der Liste. - key: "text", header: t("transactions.subject"), grow: true, min: 220, + key: "text", header: t("transactions.subject"), grow: true, min: 150, filterText: (tx) => rowLabel(tx).title, sortValue: (tx) => rowLabel(tx).title, render: (tx) => { @@ -277,7 +280,7 @@ export default function Transactions() { { // Eigene Spalte, damit die Auswahl in der Zeile steht und nicht unter dem // Text – gestapelt wurden die Zeilen hoch und die Spalte ausgefranst. - key: "category", header: t("transactions.category"), width: 210, + key: "category", header: t("transactions.category"), width: 170, filterText: (tx) => (tx.category_id ? categoryById[tx.category_id]?.name : "") || "", sortValue: (tx) => (tx.category_id ? categoryById[tx.category_id]?.name : "") || "", render: (tx) => ( @@ -295,12 +298,12 @@ export default function Transactions() { ), }, { - key: "account", header: t("transactions.account"), width: 150, + key: "account", header: t("transactions.account"), width: 130, filterText: (tx) => accountById[tx.account_id]?.name || "", render: (tx) => accountById[tx.account_id]?.name || "", }, { - key: "amount", header: t("transactions.amount"), align: "num", width: 150, + key: "amount", header: t("transactions.amount"), align: "num", width: 140, sortValue: (tx) => tx.amount_base_cents, render: (tx) => { const acc = accountById[tx.account_id]; @@ -312,18 +315,31 @@ export default function Transactions() { }, }, { - key: "actions", header: "", width: 96, fixed: true, + key: "actions", header: "", width: 118, fixed: true, render: (tx) => (
- {links[String(tx.id)] && ( + {/* Verknüpft: die beiden Seiten wieder trennen. Sonst, wenn es + überhaupt einer werden kann: zum Übertrag machen. Beides zugleich + gibt es nie – eine verknüpfte Buchung IST schon ein Paar. */} + {links[String(tx.id)] ? ( - )} + ) : tx.type !== "transfer" ? ( + + ) : null} @@ -373,6 +389,17 @@ export default function Transactions() { )} + {uebertrag && ( + setUebertrag(null)} + onFertig={ladenBuchungen} + /> + )} + {form && (