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:
@@ -7,14 +7,17 @@ import Icon from "../components/Icon";
|
||||
import IconChip from "../components/IconChip";
|
||||
import Modal from "../components/Modal";
|
||||
import IconPicker, { CATEGORY_ICONS } from "../components/IconPicker";
|
||||
import { centsToInput, parseAmountToCents } from "../format";
|
||||
|
||||
// Popup zum Pflegen der Schlagwörter einer Kategorie. Wörter werden beim Import
|
||||
// gegen Empfänger/Verwendungszweck geprüft und ordnen Buchungen automatisch zu.
|
||||
// Optional lässt sich ein fester Betrag als Zusatzbedingung angeben.
|
||||
function KeywordModal({ category, onClose }) {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const [words, setWords] = useState([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [amount, setAmount] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [editId, setEditId] = useState(null);
|
||||
const [editVal, setEditVal] = useState("");
|
||||
@@ -30,8 +33,12 @@ function KeywordModal({ category, onClose }) {
|
||||
if (!w) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.addKeyword(category.id, w);
|
||||
const body = { word: w };
|
||||
const c = parseAmountToCents(amount);
|
||||
if (c) body.amount_cents = c;
|
||||
await api.addKeyword(category.id, body);
|
||||
setInput("");
|
||||
setAmount("");
|
||||
await laden();
|
||||
} catch (err) {
|
||||
toast(err.message, "error");
|
||||
@@ -61,7 +68,7 @@ function KeywordModal({ category, onClose }) {
|
||||
const orig = words.find((x) => x.id === id);
|
||||
if (!val || (orig && val === orig.word)) { abbrechenBearbeiten(); return; }
|
||||
try {
|
||||
await api.updateKeyword(id, val);
|
||||
await api.updateKeyword(id, { word: val });
|
||||
abbrechenBearbeiten();
|
||||
await laden();
|
||||
} catch (err) {
|
||||
@@ -87,6 +94,14 @@ function KeywordModal({ category, onClose }) {
|
||||
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>
|
||||
@@ -120,6 +135,7 @@ function KeywordModal({ category, onClose }) {
|
||||
onDoubleClick={() => starteBearbeiten(w)}
|
||||
>
|
||||
{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>
|
||||
|
||||
Reference in New Issue
Block a user