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

+55 -5
View File
@@ -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() {