feat: Kontenreihenfolge selbst bestimmen

Die Liste stand in der Anlegereihenfolge, die selten die gewuenschte ist.
Neuer Endpunkt PUT /accounts/order nimmt die IDs in der gewuenschten Folge
und schreibt die Position; er muss vor /{account_id} stehen, sonst faengt
die ID-Route den Pfad ab. Fremde IDs werden abgewiesen.

In der Kontenliste zwei Pfeile je Zeile, optimistisch sortiert, damit die
Liste beim Klicken nicht springt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-21 20:36:59 +02:00
co-authored by Claude Opus 5
parent ff897c196b
commit 9642901e4e
8 changed files with 79 additions and 5 deletions
+1
View File
@@ -162,6 +162,7 @@ export const api = {
createAccount: (body) => request("/accounts", { method: "POST", body }),
updateAccount: (id, body) => request(`/accounts/${id}`, { method: "PATCH", body }),
deleteAccount: (id) => request(`/accounts/${id}`, { method: "DELETE" }),
setAccountOrder: (ids) => request("/accounts/order", { method: "PUT", body: { ids } }),
// Kategorien (Gruppen + Kategorien)
listCategories: () => request("/categories"),
+1
View File
@@ -72,6 +72,7 @@ const PATHS = {
),
"chevron-right": <path d="m9 18 6-6-6-6" />,
"chevron-down": <path d="m6 9 6 6 6-6" />,
"chevron-up": <path d="m18 15-6-6-6 6" />,
tags: (
<>
<path d="M9 5H2v7l6.29 6.29c.94.94 2.48.94 3.42 0l3.58-3.58c.94-.94.94-2.48 0-3.42L9 5Z" />
+3 -1
View File
@@ -167,7 +167,9 @@
"empty": "Noch keine Konten. Lege dein erstes Konto an.",
"deleteConfirm": "Dieses Konto und alle zugehörigen Buchungen löschen?",
"openingDateHint": "Stand am Ende dieses Tages. Buchungen bis dahin zählen nicht zum Kontostand, bleiben aber in Liste und Auswertungen. Ohne Stichtag zählt alles.",
"shortIdentWarning": "Kurze Erkennungstexte treffen leicht Fremdes: „Euro“ steckt auch in „Europa-Park“ oder „PayPal Europe“. Solche Buchungen werden dann fälschlich zu Überträgen. Nimm lieber etwas Eindeutiges wie die IBAN oder „Revolut**5064*“."
"shortIdentWarning": "Kurze Erkennungstexte treffen leicht Fremdes: „Euro“ steckt auch in „Europa-Park“ oder „PayPal Europe“. Solche Buchungen werden dann fälschlich zu Überträgen. Nimm lieber etwas Eindeutiges wie die IBAN oder „Revolut**5064*“.",
"moveUp": "Nach oben",
"moveDown": "Nach unten"
},
"accountTypes": {
"girokonto": "Girokonto",
+3 -1
View File
@@ -167,7 +167,9 @@
"empty": "No accounts yet. Create your first account.",
"deleteConfirm": "Delete this account and all its transactions?",
"openingDateHint": "Balance at the end of that day. Bookings up to then don't count toward the balance but stay in the list and reports. Without a date, everything counts.",
"shortIdentWarning": "Short match texts hit unrelated bookings: “Euro” also occurs in “Europa-Park” or “PayPal Europe”, turning those into transfers by mistake. Prefer something unique such as the IBAN."
"shortIdentWarning": "Short match texts hit unrelated bookings: “Euro” also occurs in “Europa-Park” or “PayPal Europe”, turning those into transfers by mistake. Prefer something unique such as the IBAN.",
"moveUp": "Move up",
"moveDown": "Move down"
},
"accountTypes": {
"girokonto": "Checking",
+25 -1
View File
@@ -193,6 +193,22 @@ export default function Accounts() {
}
}
// Reihenfolge per Pfeil: die Liste stand sonst in der Anlegereihenfolge.
// Optimistisch umsortieren, damit die Liste nicht bei jedem Klick springt.
async function verschiebe(index, richtung) {
const ziel = index + richtung;
if (ziel < 0 || ziel >= accounts.length) return;
const neu = [...accounts];
[neu[index], neu[ziel]] = [neu[ziel], neu[index]];
setAccounts(neu);
try {
await api.setAccountOrder(neu.map((a) => a.id));
} catch (e) {
toast(e.message, "error");
laden();
}
}
async function loeschen(acc) {
if (!(await confirm({ title: t("common.delete"), message: t("accounts.deleteConfirm"), danger: true, confirmLabel: t("common.delete") }))) return;
try {
@@ -223,7 +239,7 @@ export default function Accounts() {
<strong>{formatMoney(total, baseCurrency)}</strong>
</div>
<div className="account-list">
{accounts.map((a) => (
{accounts.map((a, i) => (
<div className="account-row" key={a.id}>
<IconChip name={a.icon} color={a.color} />
<div className="named-text grow">
@@ -236,6 +252,14 @@ export default function Accounts() {
<Money cents={a.balance_cents} currency={a.currency}
baseCents={a.balance_base_cents} baseCurrency={baseCurrency} />
<div className="row-actions">
<button className="btn sm ghost" title={t("accounts.moveUp")}
disabled={i === 0} onClick={() => verschiebe(i, -1)}>
<Icon name="chevron-up" size={15} />
</button>
<button className="btn sm ghost" title={t("accounts.moveDown")}
disabled={i === accounts.length - 1} onClick={() => verschiebe(i, 1)}>
<Icon name="chevron-down" size={15} />
</button>
<button className="btn sm ghost" title={t("common.edit")} onClick={() => openEdit(a)}>
<Icon name="edit" size={15} />
</button>