diff --git a/README.md b/README.md
index bb3b0b1..441f2ee 100644
--- a/README.md
+++ b/README.md
@@ -82,12 +82,16 @@ für eine einzelne Person.
## Tauschanfragen
-Wer an seinem Tag nicht kann, klickt den Tag im Kalender an, wählt seinen Namen, optional die
-Person und den Tag für den Tausch, und schreibt eine Notiz. Dafür ist keine Anmeldung nötig;
-pro Adresse sind zehn Anfragen je Stunde möglich. Der Tag bekommt im Kalender eine Markierung.
-Bearbeiter sehen alle Anfragen unter „Anfragen" mit Zähler in der Navigation und entscheiden:
-„Ausführen" tauscht wie gewünscht (über Kreuz oder Übernahme des Tages), „Tag öffnen" lässt
-manuell wählen, „Erledigt" und „Ablehnen" schließen die Anfrage ohne Änderung. Nach einem
+Wer an seinem Tag nicht kann, klickt den Tag im Kalender an, wählt seinen Namen und schreibt
+eine Notiz. Optional dazu: eine Person, mit der getauscht werden soll, und ein Wunschtag.
+Ohne Person bedeutet der Wunschtag, dass am Wunschtag ein noch offener Platz übernommen wird.
+Dafür ist keine Anmeldung nötig; pro Adresse sind zehn Anfragen je Stunde möglich. Der Tag
+bekommt im Kalender eine Markierung. Bearbeiter sehen alle Anfragen unter „Anfragen" mit Zähler
+in der Navigation und entscheiden: „Ausführen" tauscht wie gewünscht (über Kreuz, Übernahme
+des Tages oder Wechsel auf den offenen Platz; der frei werdende Platz wird sofort fair
+nachbesetzt), „Wunsch anpassen" trägt Person oder Wunschtag nach, etwa aus der Notiz, „Tag
+öffnen" lässt manuell wählen, „Erledigt" und „Ablehnen" schließen die Anfrage ohne Änderung.
+Rollen werden beachtet: Wer nur Hauptmoderation macht, bekommt keinen Nebenplatz. Nach einem
Tausch den Zeitraum erneut nach Nextcloud schreiben.
## Bugs melden
diff --git a/public/app.js b/public/app.js
index eb53600..19c6ed3 100644
--- a/public/app.js
+++ b/public/app.js
@@ -334,11 +334,26 @@ function fillRequestPersons(date) {
? `${fmtLong(date)}: ${assigned.map((p, i) => `${i === 0 && a.main === p.id ? 'Haupt' : 'Neben'} ${fullName(p)}`).join(', ')}`
: `${fmtLong(date)}: kein Meeting geplant`;
}
+/** Was ist am Wunschtag los? Offene Plätze sichtbar machen, damit der Wunsch passt. */
+function describeWishDay() {
+ const f = requestForm.elements;
+ const info = $('#request-wish-info');
+ const base = 'Mit Person: ihr tauscht die Tage. Ohne Person: du übernimmst am Wunschtag einen noch offenen Platz, deiner wird neu besetzt.';
+ const date = f.partnerDate.value;
+ if (!date) { info.textContent = base; return; }
+ const a = state.data.assignments[date];
+ if (!a || a.off || holidayFor(date)) { info.textContent = `${fmtDate(date)}: kein Meeting geplant.`; return; }
+ const slot = (role, id) => (id ? `${role} ${fullName(personById(id))}` : `${role} offen`);
+ info.textContent = `${fmtDate(date)}: ${slot('Haupt', a.main)}, ${slot('Neben', a.co)}.${!a.main || !a.co ? ' Ein offener Platz kann ohne Tauschpartner übernommen werden.' : ' Beide Plätze sind besetzt, nenne die Person zum Tauschen.'}`;
+}
+requestForm.elements.partnerDate.addEventListener('change', describeWishDay);
+
function openRequestDialog(date) {
const f = requestForm.elements;
requestForm.reset();
f.date.value = date || '';
fillRequestPersons(f.date.value);
+ describeWishDay();
$('#request-result').hidden = true;
$('#request-submit').disabled = false;
requestDialog.showModal();
@@ -382,7 +397,8 @@ function requestRow(r) {
const partner = r.partnerId ? personById(r.partnerId) : null;
const parts = [];
if (partner) parts.push(`Wunsch: Tausch mit ${fullName(partner)}${r.partnerDate ? ` am ${fmtDate(r.partnerDate)}` : ' (übernimmt den Tag)'}`);
- else parts.push('Kein Wunschpartner genannt');
+ else if (r.partnerDate) parts.push(`Wunsch: offenen Platz am ${fmtDate(r.partnerDate)} übernehmen`);
+ else parts.push('Kein Wunsch genannt – aus der Notiz nachtragen oder den Tag öffnen');
parts.push(`gestellt am ${new Date(r.createdAt).toLocaleDateString('de-DE')}`);
if (r.status !== 'offen') parts.push(r.status === 'erledigt' ? 'erledigt' : 'abgelehnt');
meta.textContent = parts.join(' · ');
@@ -393,10 +409,20 @@ function requestRow(r) {
actions.className = 'request-actions';
const btn = (label, cls, fn) => { const b = document.createElement('button'); b.type = 'button'; b.className = cls; b.textContent = label; b.addEventListener('click', fn); actions.appendChild(b); };
if (r.status === 'offen') {
- if (r.partnerId) btn('Ausführen', 'primary', () => run(async () => {
- const res = await api(`api/requests/${r.id}/execute`, { method: 'POST' });
- return res;
- }, 'Tausch ausgeführt. Nextcloud danach neu schreiben, damit die Termine stimmen.'));
+ if (r.partnerId || r.partnerDate) btn('Ausführen', 'primary', async () => {
+ try {
+ const res = await api(`api/requests/${r.id}/execute`, { method: 'POST' });
+ await load();
+ let msg = 'Tausch ausgeführt.';
+ if (res.filled) msg += res.filled.personId ? ` Lücke am ${fmtDate(res.filled.date)} mit ${fullName(personById(res.filled.personId))} besetzt.` : ` Lücke am ${fmtDate(res.filled.date)} konnte nicht besetzt werden, bitte im Kalender nachsehen.`;
+ toast(`${msg} Nextcloud danach neu schreiben.`);
+ } catch (err) { toast(err.message, true); }
+ });
+ btn('Wunsch anpassen', '', () => {
+ const form = li.querySelector('.wish-form');
+ form.hidden = !form.hidden;
+ if (!form.hidden) form.querySelector('input[type=date]').focus();
+ });
btn('Tag öffnen', '', () => { location.hash = '#kalender'; state.month = new Date(r.date + 'T12:00:00'); renderCalendar(); openDayDialog(r.date); });
btn('Erledigt', 'ghost', () => run(() => api(`api/requests/${r.id}`, { method: 'PUT', body: { status: 'erledigt' } }), 'Als erledigt markiert.'));
btn('Ablehnen', 'ghost', () => run(() => api(`api/requests/${r.id}`, { method: 'PUT', body: { status: 'abgelehnt' } }), 'Anfrage abgelehnt.'));
@@ -405,6 +431,30 @@ function requestRow(r) {
btn('Löschen', 'ghost', () => run(() => api(`api/requests/${r.id}`, { method: 'DELETE' })));
}
li.appendChild(actions);
+ if (r.status === 'offen') {
+ // Wunsch nachtragen: Person und/oder Wunschtag, z. B. aus der Notiz
+ const form = document.createElement('form');
+ form.className = 'wish-form';
+ form.hidden = true;
+ const sel = document.createElement('select');
+ sel.appendChild(new Option('– niemand Bestimmtes –', ''));
+ for (const p of state.data.persons.filter((x) => x.active !== false && x.id !== r.personId && (x.canMain || x.canCo)).sort((x, y) => fullName(x).localeCompare(fullName(y)))) sel.appendChild(new Option(fullName(p), p.id));
+ sel.value = r.partnerId || '';
+ const dateInput = document.createElement('input');
+ dateInput.type = 'date';
+ dateInput.value = r.partnerDate || '';
+ const save = document.createElement('button');
+ save.type = 'submit';
+ save.textContent = 'Wunsch speichern';
+ const l1 = document.createElement('label'); l1.textContent = 'Tauschen mit'; l1.appendChild(sel);
+ const l2 = document.createElement('label'); l2.textContent = 'Wunschtag'; l2.appendChild(dateInput);
+ form.append(l1, l2, save);
+ form.addEventListener('submit', (e) => {
+ e.preventDefault();
+ run(() => api(`api/requests/${r.id}`, { method: 'PUT', body: { partnerId: sel.value || null, partnerDate: dateInput.value || null } }), 'Wunsch gespeichert.');
+ });
+ li.appendChild(form);
+ }
return li;
}
function renderRequests() {
diff --git a/public/index.html b/public/index.html
index a156b3e..aa47d94 100644
--- a/public/index.html
+++ b/public/index.html
@@ -288,8 +288,9 @@
-
-
+
+
+
Mit Person: ihr tauscht die Tage. Ohne Person: du übernimmst am Wunschtag einen noch offenen Platz, deiner wird neu besetzt.
diff --git a/public/style.css b/public/style.css
index e1e68ab..45019f0 100644
--- a/public/style.css
+++ b/public/style.css
@@ -74,6 +74,9 @@ svg.icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-w
.request-meta { color: var(--text-2); font-size: 13px; }
.request-note { margin-top: 4px; font-size: 14px; }
.request-actions { display: flex; gap: 8px; flex-wrap: wrap; }
+.wish-form { flex-basis: 100%; display: flex; gap: 12px 16px; flex-wrap: wrap; align-items: end; padding: 12px 16px; margin-top: 4px; background: var(--tint-1); border-radius: var(--radius); }
+.wish-form label { flex: 1 1 180px; }
+.wish-form button { min-height: 40px; }
.viewer .day.requestable { cursor: pointer; }
.viewer .day.requestable:hover:not(.outside) { background: var(--tint-1); }
.topbar-action { margin-left: auto; background: transparent; color: var(--white); border-color: rgba(255, 255, 255, .4); min-height: 34px; padding: 0 12px; font-size: 13px; }
diff --git a/server/index.js b/server/index.js
index 9956352..c219d28 100644
--- a/server/index.js
+++ b/server/index.js
@@ -520,10 +520,27 @@ app.put('/api/requests/:id', (req, res) => {
const st = S();
const r = st.requests.find((x) => x.id === req.params.id);
if (!r) return res.status(404).json({ error: 'Anfrage nicht gefunden.' });
- const status = String(req.body?.status || '');
- if (!REQUEST_STATUS.includes(status)) return bad(res, 'Unbekannter Status.');
- r.status = status;
- r.resolvedAt = status === 'offen' ? null : new Date().toISOString();
+ const b = req.body || {};
+ if (b.status !== undefined) {
+ const status = String(b.status || '');
+ if (!REQUEST_STATUS.includes(status)) return bad(res, 'Unbekannter Status.');
+ r.status = status;
+ r.resolvedAt = status === 'offen' ? null : new Date().toISOString();
+ }
+ // Bearbeiter dürfen den Wunsch nachtragen, z. B. aus dem Notizfeld
+ if (b.partnerId !== undefined || b.partnerDate !== undefined) {
+ const v = validateRequest({
+ personId: r.personId,
+ date: r.date,
+ partnerId: b.partnerId !== undefined ? b.partnerId : r.partnerId,
+ partnerDate: b.partnerDate !== undefined ? b.partnerDate : r.partnerDate,
+ note: r.note,
+ }, st.persons, st.assignments);
+ if (v.error) return bad(res, v.error);
+ r.partnerId = v.request.partnerId;
+ r.partnerDate = v.request.partnerDate;
+ r.role = v.request.role;
+ }
store.save();
res.json(r);
});
@@ -534,11 +551,26 @@ app.post('/api/requests/:id/execute', (req, res) => {
if (!r) return res.status(404).json({ error: 'Anfrage nicht gefunden.' });
if (r.status !== 'offen') return bad(res, 'Diese Anfrage ist schon abgeschlossen.');
try {
- const { changes } = applySwap(st.assignments, r);
+ const { changes, vacated } = applySwap(st.assignments, r, personsForScheduling());
+ let filled = null;
+ if (vacated) {
+ // Die frei gewordene Stelle gleich nachbesetzen – fair, ohne die anfragende Person
+ const existing = { [vacated.date]: { ...st.assignments[vacated.date], fill: true } };
+ const absences = [...st.absences, { personId: r.personId, from: vacated.date, to: vacated.date }];
+ const history = { ...st.assignments };
+ delete history[vacated.date];
+ const generated = generateSchedule({ dates: [vacated.date], persons: personsForScheduling(), absences, existing, history, blocked: blockedDays(), historyWindowDays: st.settings.historyWindowDays });
+ const picked = generated[vacated.date]?.[vacated.role] || null;
+ if (picked) {
+ st.assignments[vacated.date][vacated.role] = picked;
+ changes.push({ date: vacated.date, role: vacated.role, from: null, to: picked });
+ }
+ filled = { date: vacated.date, role: vacated.role, personId: picked };
+ }
r.status = 'erledigt';
r.resolvedAt = new Date().toISOString();
store.save();
- res.json({ request: r, changes, assignments: st.assignments });
+ res.json({ request: r, changes, filled, assignments: st.assignments });
} catch (err) {
bad(res, err.message);
}
diff --git a/server/requests.js b/server/requests.js
index a3db11d..71c5922 100644
--- a/server/requests.js
+++ b/server/requests.js
@@ -11,6 +11,21 @@ export function roleOn(assignment, personId) {
return null;
}
+/** Darf die Person diese Rolle übernehmen? */
+export function canTake(person, role) {
+ if (!person || person.active === false) return false;
+ return role === 'main' ? person.canMain !== false : person.canCo !== false;
+}
+
+/** Offener Platz an einem Tag, den die Person übernehmen könnte: 'main', 'co' oder null. */
+export function openSlotFor(assignment, person) {
+ if (!assignment || assignment.off || !person) return null;
+ if (assignment.main === person.id || assignment.co === person.id) return null;
+ if (!assignment.main && canTake(person, 'main')) return 'main';
+ if (!assignment.co && canTake(person, 'co')) return 'co';
+ return null;
+}
+
/**
* Eingabe einer Anfrage prüfen. persons: [{id, active}], assignments: { date: {main, co, off} }.
* Liefert { error } oder { request } (ohne id/status/createdAt).
@@ -29,37 +44,64 @@ export function validateRequest(body, persons, assignments) {
if (partnerId === personId) return { error: 'Mit dir selbst kannst du nicht tauschen.' };
const partnerDate = b.partnerDate ? String(b.partnerDate) : null;
if (partnerDate && !isValidDate(partnerDate)) return { error: 'Ungültiger Wunschtag.' };
- if (partnerDate && !partnerId) return { error: 'Zum Wunschtag bitte auch die Person angeben, mit der du tauschen möchtest.' };
if (partnerDate === date) return { error: 'Der Wunschtag ist derselbe Tag.' };
+ if (partnerDate && !partnerId) {
+ // Ohne Tauschpartner heißt der Wunsch: einen noch offenen Platz am Wunschtag übernehmen
+ const me = active(personId);
+ const a = assignments[partnerDate];
+ if (!a || a.off) return { error: 'Am Wunschtag ist kein Meeting geplant.' };
+ if (a.main === personId || a.co === personId) return { error: 'Am Wunschtag bist du schon eingeteilt.' };
+ if (!openSlotFor(a, me)) return { error: 'Am Wunschtag ist kein Platz frei, den du übernehmen könntest. Nenne eine Person zum Tauschen.' };
+ }
const note = String(b.note || '').trim().slice(0, 300);
return { request: { personId, date, role, partnerId, partnerDate, note } };
}
/**
- * Tausch ausführen. Liefert { changes: [{date, role, from, to}] } und verändert assignments direkt.
+ * Tausch ausführen. Liefert { changes: [{date, role, from, to}], vacated } und verändert assignments direkt.
* Fälle:
* - Partner + Wunschtag: über Kreuz – Partner übernimmt den Tag des Anfragenden, dieser den Slot des Partners am Wunschtag.
* - Nur Partner: Partner übernimmt den Slot des Anfragenden.
- * - Ohne Partner: nicht automatisch ausführbar.
+ * - Nur Wunschtag: die Person übernimmt dort einen offenen Platz; ihr alter Platz wird frei (vacated) und
+ * sollte vom Aufrufer nachbesetzt werden.
+ * - Weder noch: nicht automatisch ausführbar.
+ * persons: [{ id, canMain, canCo, active }] für die Rollenprüfung.
*/
-export function applySwap(assignments, req) {
+export function applySwap(assignments, req, persons = []) {
+ const find = (id) => persons.find((p) => p.id === id) || null;
const a = assignments[req.date];
const role = roleOn(a, req.personId);
if (!role) throw new Error('Die Person ist an diesem Tag nicht mehr eingeteilt. Bitte den Tag von Hand anpassen.');
- if (!req.partnerId) throw new Error('Ohne Wunschpartner bitte den Tag öffnen und selbst jemanden auswählen.');
+ const me = find(req.personId);
+ const changes = [];
+
+ if (!req.partnerId) {
+ if (!req.partnerDate) throw new Error('Ohne Wunschpartner oder Wunschtag bitte den Tag öffnen und selbst jemanden auswählen.');
+ const b = assignments[req.partnerDate];
+ const slot = openSlotFor(b, me || { id: req.personId, canMain: true, canCo: true });
+ if (!slot) throw new Error('Am Wunschtag ist kein passender Platz mehr frei. Bitte den Tag von Hand anpassen.');
+ b[slot] = req.personId;
+ changes.push({ date: req.partnerDate, role: slot, from: null, to: req.personId });
+ a[role] = null;
+ changes.push({ date: req.date, role, from: req.personId, to: null });
+ return { changes, vacated: { date: req.date, role } };
+ }
+
+ const partner = find(req.partnerId);
const otherRole = role === 'main' ? 'co' : 'main';
if (a[otherRole] === req.partnerId) throw new Error('Der Wunschpartner ist an diesem Tag schon die andere Moderation. Bitte den Tag von Hand anpassen.');
- const changes = [];
+ if (partner && !canTake(partner, role)) throw new Error(`Der Wunschpartner darf keine ${role === 'main' ? 'Haupt' : 'Neben'}moderation übernehmen. Bitte den Tag von Hand anpassen.`);
if (req.partnerDate) {
const b = assignments[req.partnerDate];
const partnerRole = roleOn(b, req.partnerId);
if (!partnerRole) throw new Error('Der Wunschpartner ist am Wunschtag nicht eingeteilt. Bitte den Tag von Hand anpassen.');
const otherB = partnerRole === 'main' ? 'co' : 'main';
if (b[otherB] === req.personId) throw new Error('Die anfragende Person ist am Wunschtag schon die andere Moderation. Bitte den Tag von Hand anpassen.');
+ if (me && !canTake(me, partnerRole)) throw new Error(`Die anfragende Person darf keine ${partnerRole === 'main' ? 'Haupt' : 'Neben'}moderation übernehmen. Bitte den Tag von Hand anpassen.`);
b[partnerRole] = req.personId;
changes.push({ date: req.partnerDate, role: partnerRole, from: req.partnerId, to: req.personId });
}
a[role] = req.partnerId;
changes.push({ date: req.date, role, from: req.personId, to: req.partnerId });
- return { changes };
+ return { changes, vacated: null };
}
diff --git a/test/requests.test.js b/test/requests.test.js
index 64673b9..9a13261 100644
--- a/test/requests.test.js
+++ b/test/requests.test.js
@@ -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/);