From e57bfbabc7d3b73d877853c6ff78ed768d1825d9 Mon Sep 17 00:00:00 2001 From: Harlekin Date: Mon, 21 Sep 2026 19:53:29 +0200 Subject: [PATCH] Der eigene CC steht in der eigenen Kette Ein CC landete unter "Frueher genutzt", obwohl er dem Spieler gehoert - seine Mitgliedschaft daran war beendet, und danach allein richtete sich die Einordnung. Damit stand sein eigener CC in einem Abschnitt, der ausdruecklich fuer fremde gedacht ist, und liess sich aus der Ferne kaum in eine Kette ziehen. Jetzt entscheidet bei einem eigenen CC der Besitz: Wem er gehoert, dem gehoert er, auch wenn die Mitgliedschaft beendet wurde. Es bleibt seine Geschichte. Bei fremden CCs entscheidet weiterhin die Mitgliedschaft. Zwei Dinge fielen dabei auf: - Beim Ziehen scrollt der Browser nicht von selbst. Wer einen Eintrag von ganz unten in eine Kette weiter oben bringen wollte, sah das Ziel gar nicht. Jetzt scrollt die Seite, solange am Rand gezogen wird. - Der Besitzer konnte sich an seinem eigenen CC nicht ohne neue Buchung wieder eintragen, obwohl seine Buchung seit dem Anlegen daran haengt. Fuer ihn ist die Guthaben-Angabe jetzt freigestellt; von einem Fremden wird sie weiterhin verlangt. --- README.md | 4 +++ server/src/services/characters.ts | 8 +++++- server/test/gruppen.test.ts | 34 ++++++++++++++++++++++ web/src/components/ChainView.tsx | 48 +++++++++++++++++++++++++++++-- 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bc580e9..2e946bc 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,10 @@ Bei einem **Gruppen-CC** gilt das für die eigene Buchung: Wer ihn in seine Kett damit seinen eigenen Beitritt um - die Buchung des Erstellers bleibt unberührt, für den ist es sein eigener Platz in seiner eigenen Kette. Einreihen darf, wer an beidem beteiligt war. +**Was einem Spieler gehört, steht immer in seinen Ketten** - auch wenn seine Mitgliedschaft +daran beendet wurde. Es bleibt sein CC und seine Geschichte. Unter *Früher genutzt* landen nur +CCs, die jemand anderem gehören und zu keinem seiner Plätze mehr führen. + Am Fuß einer Kette kann ein CC stehen, der jemand anderem gehört - der Gruppen-CC, aus dem sich der Spieler herausredesignt hat. Er trägt das Kennzeichen *war dabei* samt Zeitraum und Besitzer, ist aber **genauso bedienbar wie jeder andere Eintrag**: ziehen, bearbeiten, diff --git a/server/src/services/characters.ts b/server/src/services/characters.ts index 382d7e3..c9d8b22 100644 --- a/server/src/services/characters.ts +++ b/server/src/services/characters.ts @@ -1417,10 +1417,16 @@ export async function addMember( // im laufenden Betrieb auch - es nimmt die Gruppe mit. const schonInDerKette = await warSchonInDerKette(character, playerId); + // Ebenso der Besitzer an seinem eigenen CC: Seine Buchung hängt daran, + // seit er ihn angelegt hat. Wer sich dort wieder einträgt - etwa weil er + // sich versehentlich selbst ausgetragen hat -, zahlt nicht ein zweites + // Mal für denselben Platz. + const istBesitzer = character.playerId === playerId; + // Sonst gilt: an einem laufenden CC kostet ein Platz etwas. An einem // inaktiven wird nachgetragen, was einmal war - dort ist die Angabe // freigestellt, weil die Buchung längst im Konto stehen kann. - if (!historisch && !schonInDerKette && input.useKind === undefined) { + if (!historisch && !schonInDerKette && !istBesitzer && input.useKind === undefined) { throw badRequest( 'NEED_KIND', 'Ein Platz an einem laufenden CC kostet ein Guthaben - bitte angeben, welches', diff --git a/server/test/gruppen.test.ts b/server/test/gruppen.test.ts index a7f5223..b615d81 100644 --- a/server/test/gruppen.test.ts +++ b/server/test/gruppen.test.ts @@ -735,3 +735,37 @@ describe('Einzelne Mitglieder fallen aus', () => { ); }); }); + +describe('Der eigene CC bleibt der eigene', () => { + it('lässt den Besitzer sich ohne neue Buchung wieder eintragen', async () => { + // Kommt beim Nacherfassen vor: Der Besitzer ist aus seinem eigenen CC + // ausgetragen worden. Seine Buchung hängt trotzdem daran, seit er ihn + // angelegt hat - ein zweites Mal zahlt er nicht. + const besitzer = await spielerMitPaket('Selbst ausgetragen'); + const zweiter = await spielerMitPaket('Bleibt drin'); + const { character } = await characters.createCharacter(besitzer, { name: 'Seiner' }, ctx); + await characters.addMember(character.id, zweiter, { useKind: 'CC' }, ctx); + + await characters.removeMember(character.id, besitzer, 'Versehen', ctx); + const vorher = await credits.getBalances(besitzer); + + await characters.addMember(character.id, besitzer, {}, ctx); + + assert.deepEqual(await credits.getBalances(besitzer), vorher, 'kostet ihn nichts'); + const dabei = await db.prisma.characterMember.findFirstOrThrow({ + where: { characterId: character.id, playerId: besitzer }, + }); + assert.equal(dabei.leftAt, null, 'und er ist wieder dabei'); + }); + + it('verlangt von einem Fremden weiterhin ein Guthaben', async () => { + const besitzer = await spielerMitPaket('Wirt drei'); + const fremder = await spielerMitPaket('Gast drei'); + const { character } = await characters.createCharacter(besitzer, { name: 'Seins' }, ctx); + + await assert.rejects( + () => characters.addMember(character.id, fremder, {}, ctx), + (error: Error & { code?: string }) => error.code === 'NEED_KIND', + ); + }); +}); diff --git a/web/src/components/ChainView.tsx b/web/src/components/ChainView.tsx index a8c1365..ae88fde 100644 --- a/web/src/components/ChainView.tsx +++ b/web/src/components/ChainView.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import MemberList from './MemberList'; import Notice from './Notice'; import { ApiError, api } from '../lib/api'; @@ -50,8 +50,20 @@ function eigeneMitgliedschaft(character: Character, playerId: number) { return (character.members ?? []).find((m) => m.playerId === playerId); } -/// Ein Platz, den der Spieler gerade belegt oder auf den er zurückkehren kann. +/** + * Ein Eintrag, der zu seinen Ketten gehört. + * + * Wem der CC gehört, dem gehört er - auch wenn seine Mitgliedschaft daran + * beendet wurde, etwa weil er sich aus einer Gruppe herausredesignt hat, die + * an seinem eigenen CC hing. Es bleibt seine Geschichte und sein Platz. + * + * Bei fremden CCs entscheidet die Mitgliedschaft: wer noch drin ist oder + * zurückkehren kann, hat dort einen Platz. Wer endgültig raus ist, findet + * den CC unter "Früher genutzt". + */ function istSeinPlatz(character: Character, playerId: number): boolean { + if (character.playerId === playerId) return true; + const eigene = eigeneMitgliedschaft(character, playerId); if (!eigene) return false; return eigene.leftAt === null || eigene.inactiveReason !== null; @@ -121,6 +133,38 @@ export default function ChainView({ }) { const [ziehe, setZiehe] = useState(null); const [ueber, setUeber] = useState(null); + + /** + * Während des Ziehens am Rand scrollen. + * + * Ein Browser scrollt beim Ziehen nicht von selbst. Wer einen Eintrag von + * ganz unten in eine Kette weiter oben bringen will, käme sonst nicht hin - + * er sieht das Ziel gar nicht. + */ + useEffect(() => { + if (!ziehe) return; + + const RAND = 120; + const SCHRITT = 18; + let richtung = 0; + let timer: number | null = null; + + const beiBewegung = (event: DragEvent) => { + if (event.clientY < RAND) richtung = -1; + else if (event.clientY > window.innerHeight - RAND) richtung = 1; + else richtung = 0; + }; + + timer = window.setInterval(() => { + if (richtung !== 0) window.scrollBy(0, richtung * SCHRITT); + }, 16); + + document.addEventListener('dragover', beiBewegung); + return () => { + document.removeEventListener('dragover', beiBewegung); + if (timer !== null) window.clearInterval(timer); + }; + }, [ziehe]); // Eine Meldung zum Ziehen gehört neben die Ketten, nicht an den Seitenkopf: // wer unten zieht, sieht dort oben nichts. const [meldung, setMeldung] = useState(null);