Tote CCs zurueckholen, Redesign-Beitritte und Problemfaelle

Vier Punkte aus dem Betrieb.

Ein toter CC liess sich nicht zurueckholen. Der Knopf dafuer wurde nie
angezeigt: bei Grund "Im RP gestorben" landete die Seite im Zweig "Bezahlt
reaktivieren", den der Server zu Recht abweist - ein Toter kehrt nur durch
einen Administrator zurueck. Der Dialog dafuer war fertig gebaut und wurde
schlicht nie geoeffnet. Jetzt steht an einem toten CC "Zurueckholen", fuer
Editoren stattdessen der Hinweis, dass das ein Administrator entscheidet.

Der Abgleich eines einzelnen Spielers fragte nur dessen hinterlegte SteamID
ab. Guthaben, das unter einem frueheren Konto gekauft wurde, blieb damit
unsichtbar - obwohl das Konto hinterlegt war und der Sammellauf es beruecksichtigt
hat. Jetzt werden alle Kennungen des Spielers abgefragt.

Ein Beitritt zu einem Gruppen-CC ueber ein Redesign kostete nur das Guthaben.
Damit entstand aus einem Redesign ein zusaetzlicher CC, was die Regel gerade
nicht hergibt: ein Redesign ersetzt einen CC. Jetzt muss angegeben werden,
welcher eigene CC dafuer aufgegeben wird; ein aktiver wird dabei inaktiv
gesetzt, ein bereits inaktiver bleibt unveraendert. Ueber ein CC-Paket kommt
weiterhin ein Platz hinzu, ohne Vorgaenger. Welcher CC aufgegeben wurde, haengt
am Beitritt und bleibt nachvollziehbar.

Faelle, die sich beim Eintragen nicht klaeren lassen - ein Antrag ohne
auffindbares Paket, ein fehlender GExtension-Eintrag, widerspruechliche Angaben
- koennen als Spieler angelegt und dabei als Problem vorgemerkt werden, mit
Notiz. Auch nachtraeglich setzbar und wieder abraeumbar. Die Markierung ist
folgenlos: sie sperrt nichts und veraendert weder Guthaben noch CCs. Die
Uebersicht kennzeichnet betroffene Spieler und laesst sich darauf eingrenzen.

Nebenbei: die Spielerliste gab die Markierung nicht mit aus, aufgefallen beim
Test.
This commit is contained in:
2026-09-21 00:19:48 +02:00
parent 15d683c59e
commit e65fdfc0ad
14 changed files with 794 additions and 34 deletions
+70 -5
View File
@@ -12,16 +12,25 @@ export interface PlayerInput {
steamId?: string | null;
forumName?: string | null;
notes?: string | null;
/// Anlegen, obwohl etwas nicht stimmt - siehe markProblem().
isProblem?: boolean;
problemNote?: string | null;
}
export async function searchPlayers(query: string, take = 50, skip = 0) {
export async function searchPlayers(
query: string,
take = 50,
skip = 0,
onlyProblems = false,
) {
const trimmed = query.trim();
// Wer STEAM_0:1:... eintippt, soll den Spieler finden, auch wenn intern die
// 64-Bit-Form gespeichert ist.
const asSteamId = looksLikeSteamId(trimmed) ? normalizeSteamId(trimmed) : null;
const where: Prisma.PlayerWhereInput =
trimmed.length === 0
const where: Prisma.PlayerWhereInput = {
...(onlyProblems ? { isProblem: true } : {}),
...(trimmed.length === 0
? {}
: {
OR: [
@@ -29,7 +38,8 @@ export async function searchPlayers(query: string, take = 50, skip = 0) {
{ steamId: { contains: asSteamId ?? trimmed, mode: 'insensitive' } },
{ forumName: { contains: trimmed, mode: 'insensitive' } },
],
};
}),
};
const [players, total] = await Promise.all([
prisma.player.findMany({
@@ -57,6 +67,8 @@ export async function searchPlayers(query: string, take = 50, skip = 0) {
displayName: player.displayName,
steamId: player.steamId,
forumName: player.forumName,
isProblem: player.isProblem,
problemNote: player.problemNote,
characterCount: player._count.characters,
activeCharacterCount: activeByPlayer.get(player.id) ?? 0,
balances: balances.get(player.id) as Balances,
@@ -131,6 +143,13 @@ export async function createPlayer(input: PlayerInput, ctx: ActionContext) {
steamId,
forumName: input.forumName?.trim() || null,
notes: input.notes?.trim() || null,
...(input.isProblem
? {
isProblem: true,
problemNote: input.problemNote?.trim() || null,
problemSince: new Date(),
}
: {}),
},
});
@@ -138,7 +157,11 @@ export async function createPlayer(input: PlayerInput, ctx: ActionContext) {
action: 'player.create',
targetType: 'player',
targetId: created.id,
details: { displayName: created.displayName, steamId: created.steamId },
details: {
displayName: created.displayName,
steamId: created.steamId,
isProblem: created.isProblem,
},
});
return created;
@@ -247,6 +270,48 @@ export async function deletePlayer(playerId: number, ctx: ActionContext): Promis
* Trägt ein früheres Konto nach. Steam-Kennungen werden dabei auf dieselbe
* Form gebracht wie die Hauptangabe, sonst greift der Abgleich nicht.
*/
/**
* Markiert einen Spieler als Problemfall - oder hebt die Markierung auf.
*
* Gedacht für alles, was sich im Moment nicht sauber klären lässt: ein Antrag
* ohne auffindbares Paket, ein fehlender GExtension-Eintrag, widersprüchliche
* Angaben. Der Spieler wird trotzdem angelegt, damit die Spur nicht verloren
* geht. Geklärt wird, wenn es darauf ankommt - kommt der Spieler nie wieder,
* kommt es nie darauf an.
*
* Die Markierung hat bewusst **keine** Wirkung auf Guthaben oder CCs. Sie ist
* eine Notiz mit Griff, kein Sperrvermerk.
*/
export async function markProblem(
playerId: number,
input: { isProblem: boolean; note?: string | null },
ctx: ActionContext,
) {
const existing = await prisma.player.findUnique({ where: { id: playerId } });
if (!existing) throw notFound('Spieler');
const updated = await prisma.player.update({
where: { id: playerId },
data: input.isProblem
? {
isProblem: true,
problemNote: input.note?.trim() || null,
// Ein bereits offener Fall behält seinen Anfang.
problemSince: existing.problemSince ?? new Date(),
}
: { isProblem: false, problemNote: null, problemSince: null },
});
await recordAudit(prisma, ctx, {
action: input.isProblem ? 'player.problem_marked' : 'player.problem_cleared',
targetType: 'player',
targetId: playerId,
details: { displayName: updated.displayName, note: input.note?.trim() || null },
});
return updated;
}
export async function addAlias(
playerId: number,
type: AliasType,