CC-Guthaben wird vorher umgewandelt, nicht am CC eingesetzt
Ein Redesign wird nie aus dem CC-Topf bezahlt. Wer sein Guthaben dafuer einsetzen will, tauscht es oben um und setzt es danach ein wie jedes andere Redesign. Damit faellt die Wahl CC-Guthaben an den CC-Aktionen weg; was ein Eintrag kostet, entscheidet seine Stelle in der Liste. Die Bezahlart gilt jetzt je Nutzer: bei einem Gruppen-CC hat jeder seine eigene Buchung, und umstellen liess sich bisher nur die des Erstellers. Die Guthaben-Historie laesst sich nach Topf filtern.
This commit is contained in:
+114
-28
@@ -66,6 +66,8 @@ export default function PlayerDetail() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [flash, setFlash] = useState<string | null>(null);
|
||||
/// Welcher Topf in der Historie gezeigt wird; null heisst alle.
|
||||
const [topf, setTopf] = useState<CreditKind | null>(null);
|
||||
const [dialog, setDialog] = useState<DialogState>({ type: 'none' });
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
@@ -198,6 +200,19 @@ export default function PlayerDetail() {
|
||||
);
|
||||
};
|
||||
|
||||
const umwandeln = async () => {
|
||||
const ja = await bestaetigen({
|
||||
titel: 'Ein CC-Guthaben in ein Redesign umwandeln?',
|
||||
text: 'Das kostet einen CC-Platz und bringt ein bezahltes Redesign. Einsetzen lässt es sich danach wie jedes andere Redesign.',
|
||||
knopf: 'Umwandeln',
|
||||
});
|
||||
if (!ja) return;
|
||||
await run(
|
||||
() => api(`/api/players/${playerId}/credits/convert`, { method: 'POST', body: {} }),
|
||||
'CC-Guthaben in ein Redesign umgewandelt.',
|
||||
);
|
||||
};
|
||||
|
||||
// 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.
|
||||
@@ -222,6 +237,8 @@ export default function PlayerDetail() {
|
||||
|
||||
const editor = can('EDITOR');
|
||||
const admin = can('ADMIN');
|
||||
const gezeigteBuchungen =
|
||||
topf === null ? detail.ledger : detail.ledger.filter((entry) => entry.kind === topf);
|
||||
|
||||
const playerName = detail.player.displayName;
|
||||
|
||||
@@ -418,6 +435,30 @@ export default function PlayerDetail() {
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{/* Ein Redesign wird nie aus dem CC-Topf bezahlt. Wer sein Guthaben
|
||||
dafür einsetzen will, tauscht es hier - danach ist es ein
|
||||
Redesign wie jedes andere. */}
|
||||
<span className="credit-pair">
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
disabled={busy}
|
||||
title="Ein CC-Guthaben in ein bezahltes Redesign tauschen: ein Platz weniger, ein Redesign mehr"
|
||||
onClick={() => void umwandeln()}
|
||||
>
|
||||
CC → Redesign
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost"
|
||||
disabled={busy}
|
||||
title="Die zuletzt vorgenommene Umwandlung zurücknehmen"
|
||||
onClick={() => void zugabeZuruecknehmen('convert', 'umgewandelte CC-Guthaben')}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
@@ -505,6 +546,27 @@ export default function PlayerDetail() {
|
||||
|
||||
<section className="block">
|
||||
<h2>Guthaben-Historie</h2>
|
||||
{/* Wer wissen will, wo eine einzelne Kulanz geblieben ist, soll nicht
|
||||
durch hundert Zeilen scrollen müssen. */}
|
||||
<div className="filter-row">
|
||||
{(
|
||||
[
|
||||
[null, 'Alle Töpfe'],
|
||||
['CC', KIND_LABEL.CC],
|
||||
['REDESIGN_PAID', KIND_LABEL.REDESIGN_PAID],
|
||||
['REDESIGN_KULANZ', KIND_LABEL.REDESIGN_KULANZ],
|
||||
] as const
|
||||
).map(([wert, label]) => (
|
||||
<button
|
||||
key={label}
|
||||
type="button"
|
||||
className={`btn btn-tiny ${topf === wert ? 'btn-primary' : 'btn-ghost'}`}
|
||||
onClick={() => setTopf(wert)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<thead>
|
||||
@@ -519,7 +581,7 @@ export default function PlayerDetail() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{detail.ledger.map((entry) => (
|
||||
{gezeigteBuchungen.map((entry) => (
|
||||
<tr key={entry.id} className={entry.reversedBy ? 'is-reversed' : undefined}>
|
||||
<td
|
||||
className="muted small"
|
||||
@@ -558,10 +620,12 @@ export default function PlayerDetail() {
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{detail.ledger.length === 0 && (
|
||||
{gezeigteBuchungen.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={7} className="empty">
|
||||
Noch keine Buchungen.
|
||||
{topf === null
|
||||
? 'Noch keine Buchungen.'
|
||||
: `In diesem Topf gibt es keine Buchung.`}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -570,7 +634,7 @@ export default function PlayerDetail() {
|
||||
</div>
|
||||
{detail.ledgerTotal > detail.ledger.length && (
|
||||
<p className="muted">
|
||||
{detail.ledger.length} von {detail.ledgerTotal} Buchungen angezeigt.
|
||||
{detail.ledger.length} von {detail.ledgerTotal} Buchungen geladen.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
@@ -601,7 +665,7 @@ export default function PlayerDetail() {
|
||||
submitLabel="Redesign durchführen"
|
||||
hint={`"${dialog.character.name}" wird inaktiv (redesignt), der neue CC tritt an seine Stelle.`}
|
||||
busy={busy}
|
||||
withKind={['REDESIGN_PAID', 'REDESIGN_KULANZ', 'CC']}
|
||||
withKind={['REDESIGN_PAID', 'REDESIGN_KULANZ']}
|
||||
balances={detail.balances}
|
||||
onClose={() => setDialog({ type: 'none' })}
|
||||
onSubmit={(values) =>
|
||||
@@ -643,7 +707,7 @@ export default function PlayerDetail() {
|
||||
() =>
|
||||
api(`/api/characters/${dialog.character.id}/funding`, {
|
||||
method: 'POST',
|
||||
body: { useKind },
|
||||
body: { useKind, forPlayerId: playerId },
|
||||
}),
|
||||
`Bezahlart von "${dialog.character.name}" umgebucht.`,
|
||||
)
|
||||
@@ -666,7 +730,7 @@ export default function PlayerDetail() {
|
||||
submitLabel="Redesign durchführen"
|
||||
hint={`${playerName} steigt aus "${dialog.character.name}" aus und bekommt dafür einen eigenen CC. Die übrigen Nutzer bleiben unberührt. Ein Redesign tauscht einen Platz, es kommt keiner hinzu.`}
|
||||
busy={busy}
|
||||
withKind={['REDESIGN_PAID', 'REDESIGN_KULANZ', 'CC']}
|
||||
withKind={['REDESIGN_PAID', 'REDESIGN_KULANZ']}
|
||||
balances={detail.balances}
|
||||
onClose={() => setDialog({ type: 'none' })}
|
||||
onSubmit={(values) =>
|
||||
@@ -724,8 +788,16 @@ export default function PlayerDetail() {
|
||||
{dialog.type === 'reactivatePaid' && (
|
||||
<KindChoiceModal
|
||||
title={`"${dialog.character.name}" reaktivieren`}
|
||||
hint="Dieser CC wurde redesignt und existiert im Original nicht mehr. Die Reaktivierung kostet ein Guthaben."
|
||||
kinds={['REDESIGN_KULANZ', 'REDESIGN_PAID', 'CC']}
|
||||
hint={
|
||||
dialog.character.predecessorId === null
|
||||
? 'Dieser CC steht am Anfang einer Liste - seine Rückkehr kostet einen eigenen Platz.'
|
||||
: 'Dieser CC wurde redesignt und existiert im Original nicht mehr. Die Rückkehr kostet ein Redesign.'
|
||||
}
|
||||
kinds={
|
||||
dialog.character.predecessorId === null
|
||||
? ['CC', 'REDESIGN_PAID', 'REDESIGN_KULANZ']
|
||||
: ['REDESIGN_PAID', 'REDESIGN_KULANZ', 'CC']
|
||||
}
|
||||
balances={detail.balances}
|
||||
busy={busy}
|
||||
onClose={() => setDialog({ type: 'none' })}
|
||||
@@ -1069,12 +1141,6 @@ function CharacterFormModal({
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{useKind === 'CC' && (
|
||||
<p className="muted small">
|
||||
Ein CC-Guthaben für ein Redesign wird umgewandelt: ein Platz weniger, ein
|
||||
bezahltes Redesign mehr.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1688,7 +1754,11 @@ function ReviveDeadModal({
|
||||
}) {
|
||||
const [reason, setReason] = useState('');
|
||||
const [kostet, setKostet] = useState(false);
|
||||
const [useKind, setUseKind] = useState<CreditKind>('CC');
|
||||
// Am Anfang einer Liste kostet die Rückkehr einen Platz, darüber ein
|
||||
// Redesign - das ergibt sich aus der Stelle, nicht aus der Wahl.
|
||||
const [useKind, setUseKind] = useState<CreditKind>(
|
||||
character.predecessorId === null ? 'CC' : 'REDESIGN_PAID',
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal title={`"${character.name}" zurückholen`} onClose={onClose}>
|
||||
@@ -1730,12 +1800,17 @@ function ReviveDeadModal({
|
||||
|
||||
{kostet && (
|
||||
<div className="radio-group">
|
||||
{(
|
||||
[
|
||||
['CC', 'CC-Guthaben', balances.CC],
|
||||
['REDESIGN_KULANZ', 'Kulanz-Redesign', balances.REDESIGN_KULANZ],
|
||||
['REDESIGN_PAID', 'Bezahltes Redesign', balances.REDESIGN_PAID],
|
||||
] as const
|
||||
{(character.predecessorId === null
|
||||
? ([
|
||||
['CC', 'CC-Guthaben', balances.CC],
|
||||
['REDESIGN_PAID', 'Bezahltes Redesign', balances.REDESIGN_PAID],
|
||||
['REDESIGN_KULANZ', 'Kulanz-Redesign', balances.REDESIGN_KULANZ],
|
||||
] as const)
|
||||
: ([
|
||||
['REDESIGN_PAID', 'Bezahltes Redesign', balances.REDESIGN_PAID],
|
||||
['REDESIGN_KULANZ', 'Kulanz-Redesign', balances.REDESIGN_KULANZ],
|
||||
['CC', 'CC-Guthaben', balances.CC],
|
||||
] as const)
|
||||
).map(([wert, label, stand]) => (
|
||||
<label key={wert} className="radio">
|
||||
<input
|
||||
@@ -1878,22 +1953,33 @@ function RedesignKindModal({
|
||||
onClose: () => void;
|
||||
onSubmit: (useKind: CreditKind) => void;
|
||||
}) {
|
||||
const [useKind, setUseKind] = useState<CreditKind>('REDESIGN_KULANZ');
|
||||
const [useKind, setUseKind] = useState<CreditKind>('REDESIGN_PAID');
|
||||
|
||||
// Am Anfang einer Liste bucht ein Eintrag einen Platz; das ergibt sich aus
|
||||
// der Stelle und steht nicht zur Wahl. Zu entscheiden bleibt, ob ein
|
||||
// Redesign bezahlt wurde oder auf Kulanz lief.
|
||||
const amAnfang = character.predecessorId === null;
|
||||
|
||||
return (
|
||||
<Modal title={`Bezahlart von "${character.name}"`} onClose={onClose}>
|
||||
<Notice kind="info">
|
||||
Woraus ein CC bezahlt wurde, stellt sich manchmal erst später heraus - etwa wenn eine
|
||||
Absprache auftaucht oder sich zeigt, dass ein Eintrag gar nicht der Anfang einer Kette
|
||||
war. Umgebucht wird über eine Gegenbuchung; in der Historie bleibt beides sichtbar.
|
||||
Woraus ein Redesign bezahlt wurde, stellt sich manchmal erst später heraus - etwa wenn
|
||||
eine Absprache auftaucht. Umgebucht wird über eine Gegenbuchung; in der Historie bleibt
|
||||
beides sichtbar.
|
||||
</Notice>
|
||||
|
||||
{amAnfang && (
|
||||
<Notice kind="warn">
|
||||
Dieser Eintrag steht am Anfang einer Liste und bucht damit einen eigenen Platz aus dem
|
||||
CC-Guthaben. Erst über einem anderen Eintrag wird daraus ein Redesign.
|
||||
</Notice>
|
||||
)}
|
||||
|
||||
<div className="radio-group">
|
||||
{(
|
||||
[
|
||||
['CC', 'CC-Paket (eigener Platz)', balances.CC],
|
||||
['REDESIGN_KULANZ', 'Kulanz-Redesign', balances.REDESIGN_KULANZ],
|
||||
['REDESIGN_PAID', 'Bezahltes Redesign', balances.REDESIGN_PAID],
|
||||
['REDESIGN_KULANZ', 'Kulanz-Redesign', balances.REDESIGN_KULANZ],
|
||||
] as const
|
||||
).map(([wert, label, stand]) => (
|
||||
<label key={wert} className="radio">
|
||||
|
||||
@@ -1327,3 +1327,11 @@ textarea {
|
||||
border-color: var(--warn);
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
/* Knopfreihe über einer Tabelle, etwa zum Filtern der Historie. */
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user