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:
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user