Guthaben: "von wie vielen" anzeigen und Zugaben zuruecknehmen

In den Kaesten steht jetzt neben dem offenen Stand, wie viel jemals in den
Topf kam: "0 von 3" liest sich anders als "0 von 0" - das eine heisst
aufgebraucht, das andere nie etwas gehabt. Gezaehlt werden dabei nur echte
Zugaenge: Die Ruecknahme eines Verbrauchs zaehlt dorthin, wo die stornierte
Zeile zaehlte, sonst saehe ein zurueckgenommenes Redesign wie ein
zusaetzliches Paket aus.

Neben jedem Plus steht ein Minus, das die zuletzt von Hand eingetragene
Zugabe dieser Art zurueckholt - fuer den vertippten oder doppelten Klick,
ohne in der Historie nach der richtigen Zeile zu suchen. Zurueckgenommen wird
ueber eine Gegenbuchung; bei einem CC-Paket gehen beide Zeilen mit. Aus einem
Import gebuchte Pakete bleiben aussen vor: die gehoeren zu einer Bestellung
und werden dort storniert, wo der Zusammenhang sichtbar ist. Die Rechte
folgen dem Eintragen - Kulanz bleibt bei den Admins.

Nebenbei zwei wacklige Tests festgezurrt: Die Testdatenbanken weichen auf den
naechsten Port aus, wenn einer aus einem frueheren Lauf noch haengt, und der
Sperr-Test des Abgleichs klopft nicht mehr bei GExtension an, sondern laeuft
gegen eine tote Adresse.
This commit is contained in:
2026-09-21 18:50:27 +02:00
parent 2d1c008c0a
commit f7b48a9891
11 changed files with 397 additions and 38 deletions
+23 -6
View File
@@ -1,11 +1,19 @@
import type { Balances } from '../lib/types';
function Card({ label, value, hint }: { label: string; value: number; hint?: string }) {
/**
* Ein Topf: was noch offen ist, und wovon.
*
* "0 von 3" liest sich anders als "0 von 0" - das eine heisst aufgebraucht,
* das andere nie etwas gehabt. Ohne die zweite Zahl sehen beide gleich aus.
*/
function Card({ label, value, granted }: { label: string; value: number; granted: number }) {
return (
<div className={`balance-card${value < 0 ? ' is-negative' : ''}`}>
<span className="balance-label">{label}</span>
<span className="balance-value">{value}</span>
{hint && <span className="balance-hint">{hint}</span>}
<span className="balance-value">
{value}
{granted > 0 && <span className="balance-of"> von {granted}</span>}
</span>
</div>
);
}
@@ -13,13 +21,22 @@ function Card({ label, value, hint }: { label: string; value: number; hint?: str
export default function BalanceCards({ balances }: { balances: Balances }) {
const canCreate = balances.CC >= 1;
const canRedesign = balances.redesignTotal >= 1;
const granted = balances.granted ?? { CC: 0, REDESIGN_PAID: 0, REDESIGN_KULANZ: 0 };
return (
<section className="balances" aria-label="Guthaben">
<div className="balance-grid">
<Card label="CC-Guthaben" value={balances.CC} />
<Card label="Redesign (bezahlt)" value={balances.REDESIGN_PAID} />
<Card label="Redesign (Kulanz)" value={balances.REDESIGN_KULANZ} />
<Card label="CC-Guthaben" value={balances.CC} granted={granted.CC} />
<Card
label="Redesign (bezahlt)"
value={balances.REDESIGN_PAID}
granted={granted.REDESIGN_PAID}
/>
<Card
label="Redesign (Kulanz)"
value={balances.REDESIGN_KULANZ}
granted={granted.REDESIGN_KULANZ}
/>
</div>
<div className="eligibility">
+2
View File
@@ -29,6 +29,8 @@ export interface Balances {
REDESIGN_PAID: number;
REDESIGN_KULANZ: number;
redesignTotal: number;
/// Wie viel jemals in den Topf kam, abzüglich stornierter Zugänge.
granted?: { CC: number; REDESIGN_PAID: number; REDESIGN_KULANZ: number };
}
export interface PlayerSummary {
+71 -20
View File
@@ -101,6 +101,22 @@ export default function PlayerDetail() {
}
};
/**
* Nimmt die zuletzt von Hand eingetragene Zugabe zurück.
*
* Zurückgenommen wird über eine Gegenbuchung - in der Historie bleibt
* beides stehen. Gebuchte Import-Pakete rührt das nicht an; die gehören zu
* einer Bestellung und werden dort storniert, wo der Zusammenhang steht.
*/
const zugabeZuruecknehmen = async (pfad: string, was: string) => {
if (!window.confirm(`Zuletzt eingetragenes ${was} zurücknehmen?`)) return;
await run(
() =>
api(`/api/players/${playerId}/credits/${pfad}/remove`, { method: 'POST', body: {} }),
`${was} zurückgenommen.`,
);
};
// Der Abgleich bucht nichts, er legt eine Vorschau an. Das muss die Meldung
// auch sagen, sonst wirkt der Knopf wirkungslos. Gelesen werden alle Konten
// des Spielers, deshalb kann auch ein einzelner Lauf dauern.
@@ -259,32 +275,67 @@ export default function PlayerDetail() {
{editor && (
<section className="action-bar">
<button
type="button"
className="btn"
disabled={busy}
onClick={() => setDialog({ type: 'addCredit', kind: 'cc-package' })}
>
+ CC-Paket
</button>
<button
type="button"
className="btn"
disabled={busy}
onClick={() => setDialog({ type: 'addCredit', kind: 'redesign-package' })}
>
+ Redesign-Paket
</button>
{/* Kulanz vergibt das Team, sie steht in keinem Kauf - daher nur Admins. */}
{can('ADMIN') && (
<span className="credit-pair">
<button
type="button"
className="btn"
disabled={busy}
onClick={() => setDialog({ type: 'addCredit', kind: 'kulanz' })}
onClick={() => setDialog({ type: 'addCredit', kind: 'cc-package' })}
>
+ Kulanz
+ CC-Paket
</button>
<button
type="button"
className="btn btn-ghost"
disabled={busy}
title="Das zuletzt von Hand eingetragene CC-Paket zurücknehmen"
onClick={() => void zugabeZuruecknehmen('cc-package', 'CC-Paket')}
>
−
</button>
</span>
<span className="credit-pair">
<button
type="button"
className="btn"
disabled={busy}
onClick={() => setDialog({ type: 'addCredit', kind: 'redesign-package' })}
>
+ Redesign-Paket
</button>
<button
type="button"
className="btn btn-ghost"
disabled={busy}
title="Das zuletzt von Hand eingetragene Redesign-Paket zurücknehmen"
onClick={() => void zugabeZuruecknehmen('redesign-package', 'Redesign-Paket')}
>
−
</button>
</span>
{/* Kulanz vergibt das Team, sie steht in keinem Kauf - daher nur Admins. */}
{can('ADMIN') && (
<span className="credit-pair">
<button
type="button"
className="btn"
disabled={busy}
onClick={() => setDialog({ type: 'addCredit', kind: 'kulanz' })}
>
+ Kulanz
</button>
<button
type="button"
className="btn btn-ghost"
disabled={busy}
title="Die zuletzt von Hand eingetragene Kulanz zurücknehmen"
onClick={() => void zugabeZuruecknehmen('kulanz', 'Kulanz')}
>
−
</button>
</span>
)}
<button
type="button"
+26
View File
@@ -1260,3 +1260,29 @@ textarea {
display: block;
margin-top: 0.2rem;
}
/* "3 von 5": der zweite Teil ist Beiwerk und tritt zurück. */
.balance-of {
font-size: 0.55em;
font-weight: 400;
color: var(--text-muted);
margin-left: 0.3rem;
}
/* Hinzufügen und Zurücknehmen gehören zusammen und sehen auch so aus. */
.credit-pair {
display: inline-flex;
}
.credit-pair .btn:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.credit-pair .btn:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: none;
padding-left: 0.6rem;
padding-right: 0.6rem;
}