From 7bfa45a400a774a22f03b604e138b73acdd86954 Mon Sep 17 00:00:00 2001 From: Giuseppe Lombardo Date: Wed, 12 Aug 2026 19:57:54 +0000 Subject: [PATCH] Add Putzplan calendar/absences UI --- web-dashboard/index.html | 239 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) diff --git a/web-dashboard/index.html b/web-dashboard/index.html index 7911d5e..eea30a5 100644 --- a/web-dashboard/index.html +++ b/web-dashboard/index.html @@ -282,6 +282,43 @@ .invite-status-chip.PENDING { background: var(--yellow-bg); color: var(--yellow); } .invite-status-chip.ACCEPTED { background: var(--green-bg); color: var(--green); } .invite-status-chip.EXPIRED, .invite-status-chip.REVOKED { background: #EEF0F2; color: var(--text-muted); } + + .calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 6px; + } + .calendar-weekday { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + color: var(--text-muted); + text-align: center; + padding-bottom: 4px; + } + .calendar-day { + background: var(--card-bg, #fff); + border: 1px solid var(--border); + border-radius: 8px; + min-height: 78px; + padding: 6px; + font-size: 11.5px; + display: flex; + flex-direction: column; + gap: 3px; + } + .calendar-day.calendar-day-outside { opacity: 0.35; } + .calendar-day.calendar-day-today { border-color: var(--green, #2f8f5b); border-width: 2px; } + .calendar-day-num { font-weight: 700; font-size: 12px; } + .calendar-badge { + border-radius: 5px; + padding: 1px 5px; + font-size: 10.5px; + line-height: 1.4; + white-space: normal; + } + .calendar-badge-task { background: var(--green-bg, #E4F3EA); color: var(--green, #2f8f5b); } + .calendar-badge-absence { background: var(--yellow-bg, #FDF3DC); color: var(--yellow, #B4780A); } @@ -458,6 +495,46 @@

Putzplan

+ +
+

Putzplan-Kalender & Abwesenheiten

+

+ Zeigt für alle sichtbar, wer wann für welchen Bereich zuständig ist, sowie eingetragene Abwesenheiten (Urlaub, "kann nicht"). +

+
+ +
+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+ + + +
+
+
+ +

Abwesenheiten im angezeigten Monat

+
+
@@ -647,11 +724,16 @@ if (isLandlord) initInvitePanel(); if (isLandlord) initCleaningAssignForm(); + document.getElementById('absenceUserField').style.display = isLandlord ? 'flex' : 'none'; + initTicketForm(); + initAbsenceForm(); loadData(); loadCleaningTasks(); + loadCalendar(); setInterval(loadData, REFRESH_INTERVAL_MS); setInterval(loadCleaningTasks, REFRESH_INTERVAL_MS); + setInterval(loadCalendar, REFRESH_INTERVAL_MS); } // --------------------------------------------------------------------- @@ -1123,6 +1205,163 @@ } } + // --------------------------------------------------------------------- + // PUTZPLAN-KALENDER & ABWESENHEITEN + // --------------------------------------------------------------------- + + const WEEKDAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; + let calendarMonthDate = new Date(new Date().getFullYear(), new Date().getMonth(), 1); + let calendarData = { tasks: [], absences: [] }; + + function initAbsenceForm() { + const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN'; + const userSelect = document.getElementById('absenceUser'); + if (isLandlord) { + userSelect.innerHTML = ''; + apiFetch('/tenants').then(data => { + data.tenants.forEach(t => { + const opt = document.createElement('option'); + opt.value = t.id; + opt.textContent = t.room ? `${t.fullName} (${t.room.roomNumber})` : t.fullName; + userSelect.appendChild(opt); + }); + }).catch(err => console.error('[cleaning-absences] Mieterliste laden fehlgeschlagen:', err.message)); + } + + const form = document.getElementById('absenceForm'); + const errorBox = document.getElementById('absenceFormError'); + form.onsubmit = async (e) => { + e.preventDefault(); + errorBox.style.display = 'none'; + const submitBtn = document.getElementById('absenceSubmitBtn'); + submitBtn.disabled = true; + submitBtn.textContent = 'Trägt ein…'; + try { + const startDate = document.getElementById('absenceStart').value; + const endDate = document.getElementById('absenceEnd').value; + const note = document.getElementById('absenceNote').value; + const body = { startDate, endDate, note }; + if (isLandlord) body.userId = userSelect.value; + await apiFetch('/cleaning-absences', { method: 'POST', body: JSON.stringify(body) }); + form.reset(); + loadCalendar(); + } catch (err) { + errorBox.textContent = err.message; + errorBox.style.display = 'block'; + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Eintragen'; + } + }; + + document.getElementById('calPrevBtn').onclick = () => { + calendarMonthDate = new Date(calendarMonthDate.getFullYear(), calendarMonthDate.getMonth() - 1, 1); + loadCalendar(); + }; + document.getElementById('calNextBtn').onclick = () => { + calendarMonthDate = new Date(calendarMonthDate.getFullYear(), calendarMonthDate.getMonth() + 1, 1); + loadCalendar(); + }; + } + + async function loadCalendar() { + if (!currentUser) return; + const monthStart = new Date(calendarMonthDate.getFullYear(), calendarMonthDate.getMonth(), 1); + const monthEnd = new Date(calendarMonthDate.getFullYear(), calendarMonthDate.getMonth() + 1, 0); + const from = monthStart.toISOString().slice(0, 10); + const to = monthEnd.toISOString().slice(0, 10); + try { + const data = await apiFetch(`/cleaning-calendar?from=${from}&to=${to}`); + calendarData = data; + renderCalendar(); + } catch (err) { + console.error('[cleaning-calendar] Laden fehlgeschlagen:', err.message); + } + } + + function renderCalendar() { + const weekdaysEl = document.getElementById('calendarWeekdays'); + weekdaysEl.innerHTML = ''; + WEEKDAY_LABELS.forEach(d => weekdaysEl.appendChild(el('div', 'calendar-weekday', d))); + + document.getElementById('calMonthLabel').textContent = + calendarMonthDate.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }); + + const grid = document.getElementById('calendarGrid'); + grid.innerHTML = ''; + + const year = calendarMonthDate.getFullYear(); + const month = calendarMonthDate.getMonth(); + const firstOfMonth = new Date(year, month, 1); + // Montag = 0 ... Sonntag = 6 + const leadingBlanks = (firstOfMonth.getDay() + 6) % 7; + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const todayStr = new Date().toDateString(); + + const cellDate = new Date(year, month, 1 - leadingBlanks); + const totalCells = Math.ceil((leadingBlanks + daysInMonth) / 7) * 7; + + for (let i = 0; i < totalCells; i++) { + const d = new Date(cellDate.getFullYear(), cellDate.getMonth(), cellDate.getDate() + i); + const isOutside = d.getMonth() !== month; + const cell = el('div', 'calendar-day' + (isOutside ? ' calendar-day-outside' : '') + + (d.toDateString() === todayStr ? ' calendar-day-today' : '')); + cell.appendChild(el('div', 'calendar-day-num', String(d.getDate()))); + + calendarData.tasks.filter(t => sameDay(new Date(t.dueDate), d)).forEach(t => { + const label = `${CLEANING_AREA_LABEL[t.area] || t.area}${t.assignedUser ? ' · ' + t.assignedUser.fullName.split(' ')[0] : ''}`; + cell.appendChild(el('div', 'calendar-badge calendar-badge-task', label)); + }); + calendarData.absences.filter(a => d >= stripTime(new Date(a.startDate)) && d <= stripTime(new Date(a.endDate))).forEach(a => { + cell.appendChild(el('div', 'calendar-badge calendar-badge-absence', `${a.user.fullName.split(' ')[0]} abwesend`)); + }); + + grid.appendChild(cell); + } + + const absenceList = document.getElementById('absenceList'); + absenceList.innerHTML = ''; + if (!calendarData.absences.length) { + absenceList.appendChild(el('div', 'empty-state', 'Keine Abwesenheiten in diesem Monat eingetragen.')); + } else { + const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN'; + calendarData.absences.forEach(a => { + const row = el('div', 'ticket-row'); + const left = el('div'); + left.appendChild(el('div', 'title', a.user.fullName)); + left.appendChild(el('div', 'meta-text', + `${new Date(a.startDate).toLocaleDateString('de-DE')} – ${new Date(a.endDate).toLocaleDateString('de-DE')}` + + (a.note ? ` · ${a.note}` : ''))); + row.appendChild(left); + if (isLandlord || a.user.id === currentUser.id) { + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = 'Löschen'; + btn.onclick = () => deleteAbsence(a.id); + row.appendChild(btn); + } + absenceList.appendChild(row); + }); + } + } + + function stripTime(d) { + return new Date(d.getFullYear(), d.getMonth(), d.getDate()); + } + + function sameDay(a, b) { + return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); + } + + async function deleteAbsence(id) { + try { + await apiFetch(`/cleaning-absences/${id}`, { method: 'DELETE' }); + loadCalendar(); + } catch (err) { + alert(err.message); + } + } + bootstrap();