feat: Regeln (Betrag/Transfer/Konto-Texte) + Münz-Icon + pastellgrünes Theme
Regeln: - Schlagwörter mit optionaler Betragsbedingung (z. B. „SV = 5.00 → Restaurant"); Betrags-Regeln gewinnen vor betragslosen - Transfer-Schlagwörter (Text → Transfer) auf der Konten-Seite - Konto-Identifikator-Art „Text": erkennt das Zielkonto eines Transfers aus dem Buchungstext (längster passender Text gewinnt) - Migration 0007, Endpunkte, Tests (Betrag/Transfer/Konflikt), gesamt 30 grün Optik: - App-Icon: Münzhaufen (aus Nutzer-SVG) weiß auf grünem Grund, klein gehalten - Farbschema auf pastelliges Grün (Akzent hell/dunkel + Favicon) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,80 @@ function emptyForm() {
|
||||
};
|
||||
}
|
||||
|
||||
// Globale Transfer-Schlagwörter: Text im Buchungstext -> Transfer. Das Zielkonto
|
||||
// wird über die Erkennungs-Texte je Konto (Identifikator-Art „Text") erraten.
|
||||
function TransferKeywords() {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const [words, setWords] = useState([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [amount, setAmount] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function laden() {
|
||||
try { setWords(await api.listTransferKeywords()); }
|
||||
catch (err) { toast(err.message, "error"); }
|
||||
}
|
||||
useEffect(() => { laden(); }, []);
|
||||
|
||||
async function hinzufuegen() {
|
||||
const w = input.trim();
|
||||
if (!w) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const body = { word: w };
|
||||
const c = parseAmountToCents(amount);
|
||||
if (c) body.amount_cents = c;
|
||||
await api.addTransferKeyword(body);
|
||||
setInput("");
|
||||
setAmount("");
|
||||
await laden();
|
||||
} catch (err) {
|
||||
toast(err.message, "error");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
async function entfernen(id) {
|
||||
try { await api.deleteKeyword(id); setWords((ws) => ws.filter((x) => x.id !== id)); }
|
||||
catch (err) { toast(err.message, "error"); }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card" style={{ marginTop: "var(--sp-4)" }}>
|
||||
<h2>{t("transferRules.title")}</h2>
|
||||
<p className="muted small">{t("transferRules.hint")}</p>
|
||||
<div className="kw-add">
|
||||
<input value={input} placeholder={t("transferRules.wordPlaceholder")}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); hinzufuegen(); } }} />
|
||||
<input value={amount} inputMode="decimal" placeholder={t("categories.keywordAmount")}
|
||||
style={{ flex: "0 0 120px", minWidth: 0, marginTop: 0 }}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); hinzufuegen(); } }} />
|
||||
<button className="btn" onClick={hinzufuegen} disabled={busy || !input.trim()}>
|
||||
<Icon name="plus" size={15} />
|
||||
</button>
|
||||
</div>
|
||||
{words.length === 0 ? (
|
||||
<div className="muted small">{t("transferRules.empty")}</div>
|
||||
) : (
|
||||
<div className="kw-chips">
|
||||
{words.map((w) => (
|
||||
<span className="kw-chip" key={w.id}>
|
||||
{w.word}
|
||||
{w.amount_cents ? <span className="kw-amount">= {centsToInput(w.amount_cents)}</span> : null}
|
||||
<button type="button" title={t("common.delete")} onClick={() => entfernen(w.id)}>
|
||||
<Icon name="close" size={13} />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Accounts() {
|
||||
const { t } = useTranslation();
|
||||
const { baseCurrency } = useSettings();
|
||||
@@ -165,6 +239,8 @@ export default function Accounts() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TransferKeywords />
|
||||
|
||||
{form && (
|
||||
<Modal
|
||||
title={editing ? t("accounts.edit") : t("accounts.new")}
|
||||
@@ -238,9 +314,10 @@ export default function Accounts() {
|
||||
<option value="iban">IBAN</option>
|
||||
<option value="account_no">{t("identifierKinds.account_no")}</option>
|
||||
<option value="card_no">{t("identifierKinds.card_no")}</option>
|
||||
<option value="text">{t("identifierKinds.text")}</option>
|
||||
</select>
|
||||
<input className="grow" style={{ minWidth: 0 }} value={ident.value}
|
||||
placeholder="DE.. / Nr."
|
||||
placeholder={ident.kind === "text" ? t("accounts.textIdentPlaceholder") : "DE.. / Nr."}
|
||||
onChange={(e) => {
|
||||
const next = [...form.identifiers];
|
||||
next[idx] = { ...ident, value: e.target.value };
|
||||
|
||||
Reference in New Issue
Block a user