From 78835d1e4012df2a729914e9bfba77bb89bfd29d Mon Sep 17 00:00:00 2001 From: Guido Date: Fri, 21 Aug 2026 20:42:14 +0200 Subject: [PATCH] feat: Konto-Auswahl mit Symbol und Farbe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das native zeigte nur den +// Namen im Systemstil; bei einem Dutzend Konten erkennt man seines am Symbol +// deutlich schneller. Nutzt dieselben Klassen wie CategorySelect und wird per +// Portal gerendert, damit es in Tabellen nicht abgeschnitten wird. +export default function AccountSelect({ + accounts = [], + value, + onChange, + allowNone = false, + noneLabel = "—", + placeholder = "—", + showCurrency = true, + minWidth = 0, + disabled = false, +}) { + const { t } = useTranslation(); + const selectedId = value === "" || value == null ? null : Number(value); + const [open, setOpen] = useState(false); + const [pos, setPos] = useState(null); + const btnRef = useRef(null); + const panelRef = useRef(null); + + const sel = accounts.find((a) => a.id === selectedId) || null; + + function computePos() { + const el = btnRef.current; + if (!el) return; + const r = el.getBoundingClientRect(); + const width = Math.max(r.width, 240); + const spaceBelow = window.innerHeight - r.bottom; + const spaceAbove = r.top; + const desired = 320; + let top; + let maxHeight; + if (spaceBelow >= 200 || spaceBelow >= spaceAbove) { + maxHeight = Math.min(desired, spaceBelow - 10); + top = r.bottom + 4; + } else { + maxHeight = Math.min(desired, spaceAbove - 10); + top = r.top - 4 - maxHeight; + } + let left = r.left; + if (left + width > window.innerWidth - 8) left = window.innerWidth - 8 - width; + if (left < 8) left = 8; + setPos({ left, top, width, maxHeight }); + } + + useEffect(() => { + if (!open) return undefined; + const onDown = (e) => { + if (panelRef.current?.contains(e.target) || btnRef.current?.contains(e.target)) return; + setOpen(false); + }; + const onKey = (e) => e.key === "Escape" && setOpen(false); + // Scrollen IN der Liste darf sie nicht schliessen – nur Scrollen daneben. + const onScrollResize = (e) => { + if (e?.target && panelRef.current?.contains(e.target)) return; + setOpen(false); + }; + document.addEventListener("mousedown", onDown); + document.addEventListener("keydown", onKey); + window.addEventListener("scroll", onScrollResize, true); + window.addEventListener("resize", onScrollResize); + return () => { + document.removeEventListener("mousedown", onDown); + document.removeEventListener("keydown", onKey); + window.removeEventListener("scroll", onScrollResize, true); + window.removeEventListener("resize", onScrollResize); + }; + }, [open]); + + function choose(id) { + onChange(id); + setOpen(false); + } + + const zeile = (a) => ( + + + {a.name}{showCurrency ? ` (${a.currency})` : ""} + + ); + + return ( +
+ + + {open && pos && createPortal( +
+
+ {allowNone && ( + + )} + {accounts.map((a) => ( + + ))} + {accounts.length === 0 && ( +
{t("accounts.empty")}
+ )} +
+
, + document.body, + )} +
+ ); +} diff --git a/web/src/pages/Import.jsx b/web/src/pages/Import.jsx index 2cdf8be..a822382 100644 --- a/web/src/pages/Import.jsx +++ b/web/src/pages/Import.jsx @@ -5,6 +5,7 @@ import { useSettings } from "../settings"; import { useConfirm } from "../confirm"; import { useToast } from "../toast"; import Icon from "../components/Icon"; +import AccountSelect from "../components/AccountSelect"; import CategorySelect from "../components/CategorySelect"; import DataTable from "../components/DataTable"; import { gruppenFuer } from "../kategorien"; @@ -387,9 +388,8 @@ export default function Import() {