diff --git a/backend/app/routers/mail.py b/backend/app/routers/mail.py index 23a1c58..e6d5d26 100644 --- a/backend/app/routers/mail.py +++ b/backend/app/routers/mail.py @@ -121,6 +121,23 @@ def create_sender( return absender +@router.put("/senders/{sender_id}", response_model=MailSenderOut) +def update_sender( + sender_id: int, + payload: MailSenderIn, + db: Session = Depends(get_db), + user: User = Depends(get_current_user), +): + absender = get_owned(db, MailSender, sender_id, user) + if payload.account_id is not None: + get_owned(db, Account, payload.account_id, user) + for schluessel, wert in payload.model_dump().items(): + setattr(absender, schluessel, wert) + db.commit() + db.refresh(absender) + return absender + + @router.delete("/senders/{sender_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_sender(sender_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)): db.delete(get_owned(db, MailSender, sender_id, user)) diff --git a/backend/tests/test_mail_import.py b/backend/tests/test_mail_import.py index 6c4ccd0..6e82903 100644 --- a/backend/tests/test_mail_import.py +++ b/backend/tests/test_mail_import.py @@ -321,3 +321,21 @@ def test_von_hand_weitergeleitete_mail_wird_am_betreff_erkannt(admin, monkeypatc treffer = [e for e in admin.get("/api/inbox").json() if e["payee"] == "Sunrise GmbH / Yallo"] assert treffer, "Die Werte müssen auch aus der weitergeleiteten Mail lesbar sein" assert treffer[0]["amount_cents"] == -1490 + + +def test_zuordnung_laesst_sich_aendern_und_loeschen(admin): + konto = _konto(admin) + zweites = _konto(admin, name="Zweitkonto") + quelle = _postfach(admin) + a = admin.post(f"/api/mail/sources/{quelle['id']}/senders", + json={"from_contains": "alt", "account_id": konto["id"]}).json() + + r = admin.put(f"/api/mail/senders/{a['id']}", + json={"from_contains": "neu", "account_id": zweites["id"]}) + assert r.status_code == 200, r.text + assert r.json()["from_contains"] == "neu" + assert r.json()["account_id"] == zweites["id"] + + assert admin.delete(f"/api/mail/senders/{a['id']}").status_code == 204 + uebrig = admin.get(f"/api/mail/sources/{quelle['id']}/senders").json() + assert not [x for x in uebrig if x["id"] == a["id"]] diff --git a/web/src/api.js b/web/src/api.js index fa68f07..132f139 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -182,6 +182,7 @@ export const api = { fetchMailSource: (id) => request(`/mail/sources/${id}/fetch`, { method: "POST" }), listMailSenders: (id) => request(`/mail/sources/${id}/senders`), createMailSender: (id, body) => request(`/mail/sources/${id}/senders`, { method: "POST", body }), + updateMailSender: (id, body) => request(`/mail/senders/${id}`, { method: "PUT", body }), deleteMailSender: (id) => request(`/mail/senders/${id}`, { method: "DELETE" }), // Kategorien (Gruppen + Kategorien) diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 6be29ff..4ffc90c 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -533,6 +533,8 @@ "skippedTitle": "Diese Mails wurden übergangen", "skippedHint": "Für sie passt keine Absender-Zuordnung. Trag einen Teil der Adresse oder des Betreffs unten ein, dann werden sie beim nächsten Abruf übernommen.", "fetchedNone": "Keine Mails im gewählten Ordner gefunden. Stimmt der Ordner? Beim ersten Abruf werden die letzten 14 Tage durchsucht.", - "senderHint": "Gesucht wird in Absender, Betreff und im Anfang des Textes. Damit greift es auch bei weitergeleiteten Mails, wo im Absender deine eigene Adresse steht." + "senderHint": "Gesucht wird in Absender, Betreff und im Anfang des Textes. Damit greift es auch bei weitergeleiteten Mails, wo im Absender deine eigene Adresse steht.", + "deleteSender": "Zuordnung entfernen", + "deleteSenderConfirm": "Zuordnung „{{text}}“ entfernen? Bereits erfasste Zahlungen bleiben." } } diff --git a/web/src/locales/en.json b/web/src/locales/en.json index cf2d7b8..065591a 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -533,6 +533,8 @@ "skippedTitle": "These emails were skipped", "skippedHint": "No sender mapping matched them. Enter part of the address or the subject below and they will be picked up on the next fetch.", "fetchedNone": "No emails found in the selected folder. Is the folder right? The first fetch looks at the last 14 days.", - "senderHint": "Searched in sender, subject and the start of the body. That way it also works for forwarded emails, where the sender is your own address." + "senderHint": "Searched in sender, subject and the start of the body. That way it also works for forwarded emails, where the sender is your own address.", + "deleteSender": "Remove mapping", + "deleteSenderConfirm": "Remove mapping “{{text}}”? Payments already captured stay." } } diff --git a/web/src/pages/settings/Postfaecher.jsx b/web/src/pages/settings/Postfaecher.jsx index 39e4da4..0f9a990 100644 --- a/web/src/pages/settings/Postfaecher.jsx +++ b/web/src/pages/settings/Postfaecher.jsx @@ -108,6 +108,19 @@ export default function Postfaecher() { try { await api.deleteMailSource(quelle.id); laden(); } catch (e) { toast(e.message, "error"); } } + async function absenderAendern(a, daten) { + try { await api.updateMailSender(a.id, daten); laden(); } catch (e) { toast(e.message, "error"); } + } + + async function absenderLoeschen(a) { + if (!(await confirm({ + title: t("mailbox.deleteSender"), + message: t("mailbox.deleteSenderConfirm", { text: a.from_contains || "*" }), + danger: true, confirmLabel: t("common.delete"), + }))) return; + try { await api.deleteMailSender(a.id); laden(); } catch (e) { toast(e.message, "error"); } + } + async function absenderAnlegen(quelle, daten) { try { await api.createMailSender(quelle.id, daten); @@ -132,13 +145,16 @@ export default function Postfaecher() { ? `${t("mailbox.lastError")}: ${q.last_error}` : q.last_poll_at ? t("mailbox.lastPoll") : t("mailbox.neverPolled")} -
{t("mailbox.noSenders")}
+ ) : ( + (absender[q.id] || []).map((a) => ( +