import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { api } from "../../api"; import { formatDate } from "../../format"; import { useToast } from "../../toast"; import { useConfirm } from "../../confirm"; import Icon from "../../components/Icon"; import Modal from "../../components/Modal"; // Reicht für den Alltag; wer eine andere braucht, sagt Bescheid. const WAEHRUNGEN = ["CHF", "EUR", "USD", "GBP"]; const LEER = { name: "", host: "", port: 993, security: "ssl", username: "", password: "", folder: "INBOX", done_folder: "", mark_seen: true, is_active: true, poll_minutes: 15, }; /** * Postfächer für den Mail-Abruf. * * Raiffeisen und Revolut melden Überweisungen per E-Mail. Certain holt sie ab * und macht daraus Vorschläge – gebucht wird nur, was der Nutzer abnickt. * * Kartenzahlungen im Laden melden die Banken NICHT per Mail, nur per Push in * ihrer App. Dafür gibt es die Schnellerfassung per Kurzbefehl unter * „Mein Konto". */ export default function Postfaecher() { const { t } = useTranslation(); const toast = useToast(); const confirm = useConfirm(); const [quellen, setQuellen] = useState([]); const [konten, setKonten] = useState([]); const [form, setForm] = useState(null); // null = Formular zu const [ordner, setOrdner] = useState(null); // Ergebnis des Verbindungstests const [absender, setAbsender] = useState({}); const [busy, setBusy] = useState(false); // Was beim letzten Abruf durchgefallen ist – beim Einrichten die einzige // Möglichkeit zu sehen, was in den Absender-Filter gehört. const [uebergangen, setUebergangen] = useState([]); async function laden() { try { const [q, k] = await Promise.all([api.listMailSources(), api.listAccounts()]); setQuellen(q); setKonten(k); const je = {}; for (const s of q) je[s.id] = await api.listMailSenders(s.id); setAbsender(je); } catch (e) { toast(e.message, "error"); } } useEffect(() => { laden(); }, []); const setzen = (teil) => setForm((f) => ({ ...f, ...teil })); async function speichern(e) { e.preventDefault(); setBusy(true); try { const koerper = { ...form }; if (form.id && !koerper.password) delete koerper.password; if (form.id) await api.updateMailSource(form.id, koerper); else await api.createMailSource(koerper); setForm(null); setOrdner(null); laden(); } catch (e) { toast(e.message, "error"); } finally { setBusy(false); } } async function testen(quelle) { setBusy(true); try { const r = await api.testMailSource(quelle.id); setOrdner(r.folders); toast(t("mailbox.testOk", { n: r.count })); } catch (e) { toast(e.message, "error"); } finally { setBusy(false); } } async function abrufen(quelle, tage) { setBusy(true); try { const r = await api.fetchMailSource(quelle.id, tage); // „0 neu" allein sagt nicht, ob nichts da war oder nichts passte. Und // wenn nichts da war, ist die Frage nicht „stimmt der Ordner", sondern // „wie weit wurde überhaupt gesucht" – deshalb steht das Datum dabei. toast(r.gefunden === 0 ? t("mailbox.fetchedNone", { seit: r.seit ? formatDate(r.seit) : "?" }) : t("mailbox.fetched", { gefunden: r.gefunden, neu: r.neu, unklar: r.unklar })); setUebergangen(r.ohne_zuordnung || []); laden(); } catch (e) { toast(e.message, "error"); } finally { setBusy(false); } } async function loeschen(quelle) { if (!(await confirm({ title: t("mailbox.delete"), message: t("mailbox.deleteConfirm", { name: quelle.name || quelle.host }), danger: true, confirmLabel: t("common.delete"), }))) return; 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); laden(); } catch (e) { toast(e.message, "error"); } } return (

{t("mailbox.title")}

{t("mailbox.intro")}

{quellen.map((q) => (
{q.name || q.host}
{q.username} · {q.folder} · {q.security.toUpperCase()}
{q.last_error ? `${t("mailbox.lastError")}: ${q.last_error}` : q.last_poll_at ? t("mailbox.lastPoll") : t("mailbox.neverPolled")}
{(absender[q.id] || []).length === 0 ? (

{t("mailbox.noSenders")}

) : ( (absender[q.id] || []).map((a) => ( absenderAendern(a, d)} onDelete={() => absenderLoeschen(a)} /> )) )}
absenderAnlegen(q, d)} t={t} /> {uebergangen.length > 0 && (
{t("mailbox.skippedTitle")}

{t("mailbox.skippedHint")}

    {uebergangen.map((m, i) => (
  • {m.from} — {m.subject}
  • ))}
)}
{/* Der Zeitstempel wird auch gesetzt, wenn ein Abruf nichts fand. Ohne diesen Weg blieben ältere Mails für immer ausserhalb des Fensters, obwohl sie im Postfach liegen. */}
))} {form !== null && ( { setForm(null); setOrdner(null); }} footer={ <> } >
{form.security === "none" &&

{t("mailbox.secNoneWarn")}

}

{t("mailbox.passwordHint")}

{t("mailbox.folderHint")}

)}
); } /** Eine bestehende Zuordnung: Text und Konto änderbar, dazu löschbar. */ function SenderRow({ absender, konten, t, onSave, onDelete }) { const [text, setText] = useState(absender.from_contains || ""); const [konto, setKonto] = useState(absender.account_id ? String(absender.account_id) : ""); const [waehrung, setWaehrung] = useState(absender.currency || ""); const geaendert = text !== (absender.from_contains || "") || waehrung !== (absender.currency || "") || konto !== (absender.account_id ? String(absender.account_id) : ""); return (
setText(e.target.value)} placeholder={t("mailbox.fromPlaceholder")} />
{geaendert && ( )}
); } /** Absender einem Konto zuordnen – ohne das weiss Certain nicht, wohin gebucht wird. */ function SenderForm({ konten, onAdd, t }) { const [von, setVon] = useState(""); const [konto, setKonto] = useState(""); const [waehrung, setWaehrung] = useState(""); return (
setVon(e.target.value)} />

{t("mailbox.senderHint")}

); }