Beim Durcharbeiten von Spieler A stellt sich heraus, dass er schon vor drei
Redesigns bei einem Gruppen-CC von Spieler B dabei war. Das ging bisher nicht:
"Einem inaktiven CC kann niemand beitreten" - richtig fuer den laufenden
Betrieb, falsch fuers Nacherfassen. Dort geht es nicht um jetzt, sondern um
eine Geschichte, die laengst gelaufen ist.
An einem stillgelegten CC wird deshalb nachgetragen statt beigetreten: mit
Zeitraum, und die Guthaben-Angabe ist freigestellt, weil die Buchung im Konto
stehen kann. Wer weiter unten in derselben Kette schon steht, kommt auch an
einem laufenden CC ohne neue Buchung dazu - sein Platz wurde einmal bezahlt,
und ein Redesign tauscht ihn aus. Fuer alle anderen bleibt die Angabe Pflicht.
Damit vier Kettenglieder nicht vier Klicks bedeuten, traegt ein Aufruf einen
Spieler ab einem Glied in die ganze weitere Kette ein; gebucht wird einmal.
Ein Test spielt den ganzen Fall durch: B fertig, A nachgetragen, C steigt in
der Mitte ein und wieder aus, D kommt erst am Ende dazu - und am Schluss sieht
jeder seine eigene Geschichte.
Leere Import-Vorschauen blieben in der Liste stehen, sobald alle Zeilen beim
Spieler gebucht waren. Bestaetigen liess sich so eine Huelle nicht ("nichts zu
buchen"), sie war nur im Weg. Jetzt gilt sie als erledigt - sie ist es ja.
Auch der Bestand wird beim naechsten Aufruf der Liste abgeraeumt.
Und die Spielerliste laesst sich eingrenzen: nur mit aktivem CC, nur mit
ausschliesslich stillgelegten, nur Beteiligte eines Gruppen-CCs, nur ohne
jeden CC, nur Problemfaelle. Sortiert wird nach Namen, nach der Zahl der
CC-Eintraege oder nach Anlage.
127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import { z } from 'zod';
|
|
import { CreditKind } from '@prisma/client';
|
|
import { AliasType } from '@prisma/client';
|
|
import { requireAdmin, requireAuth, requireEditor } from '../auth/guard.js';
|
|
import { listLedger } from '../services/credits.js';
|
|
import {
|
|
addAlias,
|
|
createPlayer,
|
|
deletePlayer,
|
|
getPlayerDetail,
|
|
markProblem,
|
|
removeAlias,
|
|
searchPlayers,
|
|
updatePlayer,
|
|
} from '../services/players.js';
|
|
import { contextOf, idParam, parseWith } from './helpers.js';
|
|
|
|
const playerBody = z.object({
|
|
displayName: z.string().trim().min(1).max(120),
|
|
steamId: z.string().trim().max(64).nullish(),
|
|
forumName: z.string().trim().max(120).nullish(),
|
|
notes: z.string().trim().max(2000).nullish(),
|
|
isProblem: z.boolean().optional(),
|
|
problemNote: z.string().trim().max(1000).nullish(),
|
|
});
|
|
|
|
export async function playerRoutes(app: FastifyInstance): Promise<void> {
|
|
app.get('/api/players', { preHandler: requireAuth }, async (request) => {
|
|
const query = parseWith(
|
|
z.object({
|
|
q: z.string().trim().max(120).default(''),
|
|
take: z.coerce.number().int().min(1).max(200).default(50),
|
|
skip: z.coerce.number().int().min(0).default(0),
|
|
filter: z
|
|
.enum(['all', 'problems', 'group', 'active', 'onlyInactive', 'none'])
|
|
.default('all'),
|
|
sort: z.enum(['name', 'characters', 'newest']).default('name'),
|
|
/// Alte Schreibweise, damit gespeicherte Adressen weiter greifen.
|
|
problems: z.enum(['true', 'false']).optional(),
|
|
}),
|
|
request.query,
|
|
);
|
|
|
|
const filter = query.problems === 'true' ? 'problems' : query.filter;
|
|
return searchPlayers(query.q, query.take, query.skip, filter, query.sort);
|
|
});
|
|
|
|
app.get('/api/players/:id', { preHandler: requireAuth }, async (request) => {
|
|
const { id } = parseWith(idParam, request.params);
|
|
return getPlayerDetail(id);
|
|
});
|
|
|
|
app.get('/api/players/:id/ledger', { preHandler: requireAuth }, async (request) => {
|
|
const { id } = parseWith(idParam, request.params);
|
|
const query = parseWith(
|
|
z.object({
|
|
kind: z.nativeEnum(CreditKind).optional(),
|
|
take: z.coerce.number().int().min(1).max(500).default(100),
|
|
skip: z.coerce.number().int().min(0).default(0),
|
|
}),
|
|
request.query,
|
|
);
|
|
return listLedger({ playerId: id, ...query });
|
|
});
|
|
|
|
app.post('/api/players', { preHandler: requireEditor }, async (request, reply) => {
|
|
const body = parseWith(playerBody, request.body);
|
|
const created = await createPlayer(body, contextOf(request));
|
|
reply.code(201);
|
|
return created;
|
|
});
|
|
|
|
app.patch('/api/players/:id', { preHandler: requireEditor }, async (request) => {
|
|
const { id } = parseWith(idParam, request.params);
|
|
// Die Problem-Markierung läuft über die eigene Route, damit ein Klären
|
|
// oder Markieren als solches im Protokoll steht und nicht als "geändert".
|
|
const body = parseWith(playerBody.omit({ isProblem: true, problemNote: true }).partial(), request.body);
|
|
return updatePlayer(id, body, contextOf(request));
|
|
});
|
|
|
|
/**
|
|
* Einen Spieler als Problemfall markieren oder die Markierung aufheben.
|
|
* Reine Merkhilfe - an Guthaben und CCs ändert sich dadurch nichts.
|
|
*/
|
|
app.post('/api/players/:id/problem', { preHandler: requireEditor }, async (request) => {
|
|
const { id } = parseWith(idParam, request.params);
|
|
const body = parseWith(
|
|
z.object({
|
|
isProblem: z.boolean(),
|
|
note: z.string().trim().max(1000).nullish(),
|
|
}),
|
|
request.body,
|
|
);
|
|
return markProblem(id, body, contextOf(request));
|
|
});
|
|
|
|
// Frühere Steam- und Forumskonten eines Spielers.
|
|
app.post('/api/players/:id/aliases', { preHandler: requireEditor }, async (request, reply) => {
|
|
const { id } = parseWith(idParam, request.params);
|
|
const body = parseWith(
|
|
z.object({
|
|
type: z.nativeEnum(AliasType),
|
|
value: z.string().trim().min(1).max(120),
|
|
note: z.string().trim().max(200).optional(),
|
|
}),
|
|
request.body,
|
|
);
|
|
|
|
const created = await addAlias(id, body.type, body.value, body.note, contextOf(request));
|
|
reply.code(201);
|
|
return created;
|
|
});
|
|
|
|
app.delete('/api/players/aliases/:id', { preHandler: requireEditor }, async (request) => {
|
|
const { id } = parseWith(idParam, request.params);
|
|
await removeAlias(id, contextOf(request));
|
|
return { ok: true };
|
|
});
|
|
|
|
app.delete('/api/players/:id', { preHandler: requireAdmin }, async (request) => {
|
|
const { id } = parseWith(idParam, request.params);
|
|
await deletePlayer(id, contextOf(request));
|
|
return { ok: true };
|
|
});
|
|
}
|