Tauschanfragen gegen offene Plätze erlauben und Lücken nachbesetzen

Ein Wunschtag ohne Tauschpartner bedeutet jetzt: die Person übernimmt
dort einen offenen Platz; ihr alter Platz wird beim Ausführen sofort
fair nachbesetzt, ohne sie selbst. Rollen werden bei allen Tauschformen
geprüft. Bearbeiter können Person und Wunschtag einer Anfrage nachtragen,
etwa aus der Notiz. Der Anfragedialog zeigt, was am Wunschtag los ist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ferdi2goandClaude Fable 5.1 committed 2026-10-15 11:55:53 +00:00
1 parent e8ab95159e
commit 42d495c54e
7 files changed
+196 -30

No files matched your search

+38 -4
View File
@@ -1,13 +1,15 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { validateRequest, applySwap, roleOn } from '../server/requests.js';
import { validateRequest, applySwap, roleOn, openSlotFor, canTake } from '../server/requests.js';
const persons = [{ id: 'a', active: true }, { id: 'b', active: true }, { id: 'c', active: true }, { id: 'x', active: false }];
const persons = [{ id: 'a', active: true }, { id: 'b', active: true }, { id: 'c', active: true }, { id: 'x', active: false }, { id: 'm', active: true, canMain: true, canCo: false }];
const plan = () => ({
'2026-10-05': { main: 'a', co: 'b', locked: false },
'2026-10-06': { main: 'c', co: 'a', locked: false },
'2026-10-07': { main: 'b', co: 'c', locked: false },
'2026-10-08': { off: true, main: null, co: null },
'2026-10-09': { main: null, co: 'c', locked: false },
'2026-10-12': { main: 'b', co: null, locked: false },
});
test('roleOn', () => {
@@ -26,7 +28,11 @@ test('validateRequest prüft Person, Tag, Einteilung und Partner', () => {
assert.match(validateRequest({ personId: 'a', date: 'gestern' }, persons, p).error, /Ungültiger Tag/);
assert.match(validateRequest({ personId: 'c', date: '2026-10-05' }, persons, p).error, /nicht eingeteilt/);
assert.match(validateRequest({ personId: 'a', date: '2026-10-05', partnerId: 'a' }, persons, p).error, /selbst/);
assert.match(validateRequest({ personId: 'a', date: '2026-10-05', partnerDate: '2026-10-07' }, persons, p).error, /Person angeben/);
assert.match(validateRequest({ personId: 'a', date: '2026-10-05', partnerDate: '2026-10-07' }, persons, p).error, /kein Platz frei/);
assert.match(validateRequest({ personId: 'a', date: '2026-10-05', partnerDate: '2026-10-08' }, persons, p).error, /kein Meeting/);
assert.match(validateRequest({ personId: 'a', date: '2026-10-05', partnerDate: '2026-10-06' }, persons, p).error, /schon eingeteilt/);
const open = validateRequest({ personId: 'a', date: '2026-10-05', partnerDate: '2026-10-09' }, persons, p);
assert.deepEqual(open.request, { personId: 'a', date: '2026-10-05', role: 'main', partnerId: null, partnerDate: '2026-10-09', note: '' });
assert.match(validateRequest({ personId: 'a', date: '2026-10-05', partnerId: 'b', partnerDate: '2026-10-05' }, persons, p).error, /derselbe/);
const ok = validateRequest({ personId: 'a', date: '2026-10-05', partnerId: 'c', partnerDate: '2026-10-07', note: ' Zahnarzt ' }, persons, p);
assert.deepEqual(ok.request, { personId: 'a', date: '2026-10-05', role: 'main', partnerId: 'c', partnerDate: '2026-10-07', note: 'Zahnarzt' });
@@ -48,8 +54,36 @@ test('applySwap: Partner übernimmt den Tag', () => {
assert.deepEqual([p['2026-10-05'].main, p['2026-10-05'].co], ['a', 'c']);
});
test('openSlotFor und canTake beachten Rollen', () => {
const p = plan();
assert.equal(openSlotFor(p['2026-10-09'], { id: 'a', canMain: true, canCo: true }), 'main');
assert.equal(openSlotFor(p['2026-10-12'], { id: 'a', canMain: true, canCo: true }), 'co');
assert.equal(openSlotFor(p['2026-10-12'], { id: 'm', canMain: true, canCo: false }), null, 'Nur-Haupt bekommt keinen Neben-Platz');
assert.equal(openSlotFor(p['2026-10-09'], { id: 'c', canMain: true, canCo: true }), null, 'schon eingeteilt');
assert.equal(openSlotFor(p['2026-10-05'], { id: 'c', canMain: true, canCo: true }), null, 'voll');
assert.ok(canTake({ id: 'm', canMain: true, canCo: false }, 'main') && !canTake({ id: 'm', canMain: true, canCo: false }, 'co'));
assert.ok(!canTake({ id: 'x', active: false }, 'main'));
});
test('applySwap: offenen Platz übernehmen, alter Platz wird frei gemeldet', () => {
const p = plan();
const { changes, vacated } = applySwap(p, { personId: 'a', date: '2026-10-05', partnerId: null, partnerDate: '2026-10-09' }, persons);
assert.equal(p['2026-10-09'].main, 'a');
assert.equal(p['2026-10-05'].main, null);
assert.deepEqual(vacated, { date: '2026-10-05', role: 'main' });
assert.equal(changes.length, 2);
assert.throws(() => applySwap(plan(), { personId: 'a', date: '2026-10-05', partnerId: null, partnerDate: '2026-10-07' }, persons), /kein passender Platz/);
});
test('applySwap prüft Rollen der Beteiligten', () => {
const p = plan();
// m darf keine Nebenmoderation: Übernahme von b's Neben-Platz am 05.10. scheitert
assert.throws(() => applySwap(p, { personId: 'b', date: '2026-10-05', partnerId: 'm', partnerDate: null }, persons), /keine Nebenmoderation/);
assert.equal(p['2026-10-05'].co, 'b', 'nichts verändert');
});
test('applySwap lehnt unmögliche Fälle verständlich ab', () => {
assert.throws(() => applySwap(plan(), { personId: 'a', date: '2026-10-05', partnerId: null }), /Wunschpartner/);
assert.throws(() => applySwap(plan(), { personId: 'a', date: '2026-10-05', partnerId: null }), /Wunschpartner oder Wunschtag/);
assert.throws(() => applySwap(plan(), { personId: 'c', date: '2026-10-05', partnerId: 'b' }), /nicht mehr eingeteilt/);
assert.throws(() => applySwap(plan(), { personId: 'a', date: '2026-10-05', partnerId: 'b' }), /andere Moderation/);
assert.throws(() => applySwap(plan(), { personId: 'a', date: '2026-10-05', partnerId: 'c', partnerDate: '2026-10-08' }), /am Wunschtag nicht eingeteilt/);