Files
certain/web/src/components/Dubletten.jsx
T
GuidoandClaude Opus 5 335a7d55c8 feat: Gleich aussehende Buchungen stehen in der Arbeitsliste
Bisher fand man Dubletten nur zufaellig beim Durchscrollen der Buchungsliste –
oder ich habe ein Skript dafuer geschrieben, und der Nutzer musste sich auf
einen Server einloggen, um seine eigenen Zahlen zu reparieren. Beides ist
falsch.

Die Arbeitsliste zeigt sie jetzt: gleiches Konto, gleicher Tag, gleicher
Betrag, gleicher Text. Je Zeile steht die Kennung der Bank dabei, und die
entscheidet die Frage – dieselbe Kennung heisst sicher dieselbe Zahlung,
verschiedene heissen, die Bank hat sie als verschieden gemeldet.

Entschieden wird bewusst NICHTS von selbst. Drei Aufrundungen ueber 1.00 am
selben Tag sind echt, drei gleiche Einkaeufe koennen es auch sein; das weiss
nur der Nutzer. Certain meldet und laesst loeschen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:32:40 +02:00

93 lines
3.2 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { api } from "../api";
import Icon from "./Icon";
import { useToast } from "../toast";
import { useConfirm } from "../confirm";
import { formatDate, formatMoney } from "../format";
// Buchungen, die gleich aussehen: gleiches Konto, gleicher Tag, gleicher
// Betrag, gleicher Text.
//
// Entschieden wird hier NICHTS. Drei Aufrundungen über 1.00 am selben Tag sind
// echt, drei gleiche Einkäufe können es auch sein – das weiss nur der Nutzer.
// Deshalb steht zu jeder Zeile die Kennung der Bank: Dieselbe Kennung heisst
// sicher dieselbe Zahlung, verschiedene heissen, die Bank hat sie als
// verschieden gemeldet.
export default function Dubletten({ accountById, onChange }) {
const { t } = useTranslation();
const toast = useToast();
const confirm = useConfirm();
const [gruppen, setGruppen] = useState(null);
async function laden() {
try {
setGruppen((await api.duplicateTransactions()).gruppen || []);
} catch {
setGruppen([]); // still ignorieren – nicht kritisch
}
}
useEffect(() => { laden(); }, []);
async function loeschen(zeile) {
if (!(await confirm({
title: t("dupes.delete"),
message: t("dupes.deleteConfirm"),
danger: true,
confirmLabel: t("dupes.delete"),
}))) return;
try {
await api.deleteTransaction(zeile.id);
await laden();
onChange?.();
} catch (e) {
toast(e.message, "error");
}
}
if (!gruppen?.length) return null;
return (
<div className="card">
<div className="card-head">
<Icon name="alert" size={18} />
<h2>{t("dupes.title", { n: gruppen.length })}</h2>
</div>
<p className="muted small">{t("dupes.intro")}</p>
<div className="account-list">
{gruppen.map((g) => (
<div className="dubletten-gruppe" key={`${g.account_id}-${g.datum}-${g.amount_cents}`}>
<div className="named-sub">
{formatDate(g.datum)} · {accountById[g.account_id]?.name || "—"} ·{" "}
<strong>
{formatMoney(g.typ === "income" ? g.amount_cents : -g.amount_cents,
accountById[g.account_id]?.currency)}
</strong>
{" · "}{t("dupes.count", { n: g.zeilen.length })}
</div>
{g.zeilen.map((z) => (
<div className="dubletten-zeile" key={z.id}>
<span className="ellipsis selectable">
{z.payee || z.description || t("inbox.noText")}
</span>
<span className="badge">{t(`todo.from.${z.herkunft}`, {
defaultValue: z.herkunft })}</span>
{/* Die Kennung entscheidet die Frage – deshalb steht sie da,
auch wenn sie hässlich ist. */}
<span className="muted small selectable nowrap">
{z.kennung || t("dupes.noRef")}
</span>
<button className="btn sm danger" onClick={() => loeschen(z)}>
<Icon name="trash" size={14} />
</button>
</div>
))}
</div>
))}
</div>
</div>
);
}