From 007ed74527d64c5fc7f601dca29e9fb60c8c3be3 Mon Sep 17 00:00:00 2001 From: Giuseppe Lombardo Date: Wed, 12 Aug 2026 21:28:30 +0000 Subject: [PATCH] index.html final korrigiert (byte-exact reupload) --- web-dashboard/index.html | 960 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 960 insertions(+) diff --git a/web-dashboard/index.html b/web-dashboard/index.html index eea30a5..06b42bd 100644 --- a/web-dashboard/index.html +++ b/web-dashboard/index.html @@ -319,6 +319,15 @@ } .calendar-badge-task { background: var(--green-bg, #E4F3EA); color: var(--green, #2f8f5b); } .calendar-badge-absence { background: var(--yellow-bg, #FDF3DC); color: var(--yellow, #B4780A); } + + .inventory-thumb { + width: 48px; + height: 48px; + object-fit: cover; + border-radius: 6px; + border: 1px solid var(--border); + } + .inventory-thumb-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; } @@ -535,6 +544,169 @@

Abwesenheiten im angezeigten Monat

+ +
+

WG-Kasse (Ausgaben-Teiler)

+

+ Auslagen wie Spülmittel oder Klopapier eintragen — der Betrag wird automatisch unter allen Mitbewohnern geteilt. +

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

Inventarverwaltung

+ +
+
+ +
+

Übergabeprotokolle

+ +
+
+ +
+

Smart-Lock-Gastcodes

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

Küchen-/Schrankplaner

+ +
+ +

Müll-Kalender

+ +
+
+ +
+

Dokumenten-Safe & Tutorials

+ +
+
@@ -554,6 +726,13 @@ const TICKET_STATUS_LABEL = { OPEN: 'Gemeldet', IN_PROGRESS: 'In Bearbeitung', RESOLVED: 'Erledigt', CLOSED: 'Geschlossen' }; const CLEANING_AREA_LABEL = { BATHROOM_1: 'Bad 1', BATHROOM_2: 'Bad 2', KITCHEN: 'Wohnküche', HALLWAY_LAUNDRY: 'Waschraum/Flur' }; const CLEANING_STATUS_LABEL = { PENDING: 'Offen', COMPLETED: 'Erledigt', VERIFIED: 'Bestätigt', MISSED: 'Verpasst' }; + const EXPENSE_CATEGORY_LABEL = { HAUSHALT: 'Haushalt', LEBENSMITTEL_GEMEINSAM: 'Lebensmittel (gemeinsam)', REPARATUR_VORAUSLAGE: 'Reparatur-Vorauslage', SONSTIGES: 'Sonstiges' }; + const ITEM_CONDITION_LABEL = { NEW: 'Neu', GOOD: 'Gut', WEAR_AND_TEAR: 'Abgenutzt', DAMAGED: 'Beschädigt' }; + const HANDOVER_TYPE_LABEL = { MOVE_IN: 'Einzug', MOVE_OUT: 'Auszug' }; + const GUEST_CODE_STATUS_LABEL = { ACTIVE: 'Aktiv', EXPIRED: 'Abgelaufen', REVOKED: 'Widerrufen' }; + const STORAGE_LOCATION_LABEL = { FRIDGE: 'Kühlschrank', FREEZER: 'Gefrierfach', KITCHEN_CABINET_1: 'Küchenschrank 1', KITCHEN_CABINET_2: 'Küchenschrank 2', KITCHEN_CABINET_3: 'Küchenschrank 3', PANTRY: 'Vorratsschrank' }; + const TRASH_TYPE_LABEL = { RESTMUELL: 'Restmüll', BIOMUELL: 'Biomüll', GELBER_SACK: 'Gelber Sack', PAPIER: 'Papier', GLAS: 'Glas' }; + const DOCUMENT_CATEGORY_LABEL = { HAUSORDNUNG: 'Hausordnung', WLAN: 'WLAN', VERTRAG: 'Vertrag', TUTORIAL: 'Tutorial', SONSTIGES: 'Sonstiges' }; function formatEuro(n) { return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(n || 0); @@ -726,14 +905,40 @@ document.getElementById('absenceUserField').style.display = isLandlord ? 'flex' : 'none'; + document.getElementById('inventoryFormCard').style.display = isLandlord ? 'block' : 'none'; + document.getElementById('handoverFormCard').style.display = isLandlord ? 'block' : 'none'; + document.getElementById('storageFormCard').style.display = isLandlord ? 'block' : 'none'; + document.getElementById('trashFormCard').style.display = isLandlord ? 'block' : 'none'; + document.getElementById('documentFormCard').style.display = isLandlord ? 'block' : 'none'; + initTicketForm(); initAbsenceForm(); + initExpenseForm(); + if (isLandlord) initInventoryForm(); + if (isLandlord) initHandoverForm(); + initGuestCodeForm(); + if (isLandlord) initStorageForms(); + if (isLandlord) initDocumentForm(); + loadData(); loadCleaningTasks(); loadCalendar(); + loadExpenses(); + loadInventory(); + loadHandoverProtocols(); + loadGuestCodes(); + loadStorage(); + loadDocuments(); + setInterval(loadData, REFRESH_INTERVAL_MS); setInterval(loadCleaningTasks, REFRESH_INTERVAL_MS); setInterval(loadCalendar, REFRESH_INTERVAL_MS); + setInterval(loadExpenses, REFRESH_INTERVAL_MS); + setInterval(loadInventory, REFRESH_INTERVAL_MS); + setInterval(loadHandoverProtocols, REFRESH_INTERVAL_MS); + setInterval(loadGuestCodes, REFRESH_INTERVAL_MS); + setInterval(loadStorage, REFRESH_INTERVAL_MS); + setInterval(loadDocuments, REFRESH_INTERVAL_MS); } // --------------------------------------------------------------------- @@ -847,6 +1052,7 @@ lastMyRoom = data.myRoom || null; populateRoomSelect(); populateTicketRoomSelect(); + populateInventoryRoomSelect(); render(data); } catch (err) { if (err.message === 'Nicht authentifiziert' || err.message === 'Token ungültig oder abgelaufen') { @@ -1362,6 +1568,760 @@ } } + // --------------------------------------------------------------------- + // WG-KASSE (AUSGABEN-TEILER) + // --------------------------------------------------------------------- + + function populateExpenseCategorySelect() { + const select = document.getElementById('expenseCategory'); + select.innerHTML = ''; + Object.keys(EXPENSE_CATEGORY_LABEL).forEach(key => { + const opt = document.createElement('option'); + opt.value = key; + opt.textContent = EXPENSE_CATEGORY_LABEL[key]; + select.appendChild(opt); + }); + } + + function initExpenseForm() { + populateExpenseCategorySelect(); + const form = document.getElementById('expenseForm'); + const errorBox = document.getElementById('expenseFormError'); + form.onsubmit = async (e) => { + e.preventDefault(); + errorBox.style.display = 'none'; + const submitBtn = document.getElementById('expenseSubmitBtn'); + submitBtn.disabled = true; + submitBtn.textContent = 'Speichert…'; + try { + const amount = document.getElementById('expenseAmount').value; + const description = document.getElementById('expenseDescription').value.trim(); + const category = document.getElementById('expenseCategory').value; + await apiFetch('/expenses', { method: 'POST', body: JSON.stringify({ amount, description, category }) }); + form.reset(); + populateExpenseCategorySelect(); + loadExpenses(); + } catch (err) { + errorBox.textContent = err.message; + errorBox.style.display = 'block'; + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Eintragen'; + } + }; + } + + async function loadExpenses() { + if (!currentUser) return; + try { + const [expensesData, balanceData] = await Promise.all([ + apiFetch('/expenses'), + apiFetch('/expenses/balance'), + ]); + renderExpenseBalance(balanceData.balance); + renderExpenses(expensesData.expenses); + } catch (err) { + console.error('[expenses] Laden fehlgeschlagen:', err.message); + } + } + + function renderExpenseBalance(balance) { + const grid = document.getElementById('expenseBalanceGrid'); + grid.innerHTML = ''; + if (!balance.length) { + grid.appendChild(el('div', 'empty-state', 'Aktuell keine offenen Salden. 🎉')); + return; + } + balance.forEach(b => { + const net = b.isOwed - b.owes; + const card = el('div', `room-card ${net > 0 ? 'GREEN' : (net < 0 ? 'RED' : '')}`); + const top = el('div', 'room-card-top'); + top.appendChild(el('div', 'room-name', b.user.fullName)); + card.appendChild(top); + const owesRow = el('div', 'amount-row'); + owesRow.appendChild(el('span', 'label', 'Schuldet')); + owesRow.appendChild(el('span', 'value', formatEuro(b.owes))); + card.appendChild(owesRow); + const owedRow = el('div', 'amount-row'); + owedRow.appendChild(el('span', 'label', 'Bekommt')); + owedRow.appendChild(el('span', 'value', formatEuro(b.isOwed))); + card.appendChild(owedRow); + grid.appendChild(card); + }); + } + + function renderExpenses(expenses) { + const list = document.getElementById('expenseList'); + list.innerHTML = ''; + if (!expenses.length) { + list.appendChild(el('div', 'empty-state', 'Noch keine Ausgaben erfasst.')); + return; + } + expenses.forEach(x => { + const row = el('div', 'ticket-row'); + const left = el('div'); + left.appendChild(el('div', 'title', `${x.description} · ${formatEuro(x.amount)}`)); + left.appendChild(el('div', 'meta-text', + `${x.payer.fullName} · ${EXPENSE_CATEGORY_LABEL[x.category] || x.category} · ${new Date(x.createdAt).toLocaleDateString('de-DE')}`)); + const openShares = x.shares.filter(s => s.settlementStatus === 'OPEN'); + if (openShares.length) { + const shareText = openShares.map(s => `${s.user.fullName}: ${formatEuro(s.shareAmount)}`).join(', '); + left.appendChild(el('div', 'meta-text', 'Offen: ' + shareText)); + } + row.appendChild(left); + + const right = el('div'); + right.style.display = 'flex'; + right.style.gap = '8px'; + right.style.flexWrap = 'wrap'; + const myShare = x.shares.find(s => s.user.id === currentUser.id && s.settlementStatus === 'OPEN'); + if (myShare) { + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = 'Meinen Anteil begleichen'; + btn.onclick = () => settleExpenseShare(myShare.id); + right.appendChild(btn); + } + const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN'; + if (isLandlord || x.payer.id === currentUser.id) { + openShares.forEach(s => { + if (s.user.id === currentUser.id) return; // schon oben abgedeckt + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = `${s.user.fullName.split(' ')[0]}: als bezahlt markieren`; + btn.onclick = () => settleExpenseShare(s.id); + right.appendChild(btn); + }); + } + row.appendChild(right); + list.appendChild(row); + }); + } + + async function settleExpenseShare(id) { + try { + await apiFetch(`/expense-shares/${id}`, { method: 'PATCH' }); + loadExpenses(); + } catch (err) { + alert(err.message); + } + } + + // --------------------------------------------------------------------- + // INVENTARVERWALTUNG + // --------------------------------------------------------------------- + + function populateInventoryRoomSelect() { + const roomSelect = document.getElementById('inventoryRoom'); + if (!roomSelect) return; + roomSelect.innerHTML = ''; + const noneOpt = document.createElement('option'); + noneOpt.value = ''; + noneOpt.textContent = 'Gemeinschaftsfläche'; + roomSelect.appendChild(noneOpt); + lastRooms.forEach(r => { + const opt = document.createElement('option'); + opt.value = r.id; + opt.textContent = r.roomNumber; + roomSelect.appendChild(opt); + }); + } + + function populateInventoryConditionSelect() { + const condSelect = document.getElementById('inventoryCondition'); + condSelect.innerHTML = ''; + Object.keys(ITEM_CONDITION_LABEL).forEach(key => { + const opt = document.createElement('option'); + opt.value = key; + opt.textContent = ITEM_CONDITION_LABEL[key]; + condSelect.appendChild(opt); + }); + } + + const MAX_INVENTORY_PHOTOS = 6; + let inventoryPhotoDataUrls = []; + + function readFileAsDataUrl(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(file); + }); + } + + function renderInventoryPhotoPreview() { + const preview = document.getElementById('inventoryPhotoPreview'); + preview.innerHTML = ''; + inventoryPhotoDataUrls.forEach((dataUrl, idx) => { + const wrap = el('div'); + wrap.style.position = 'relative'; + const img = document.createElement('img'); + img.src = dataUrl; + img.className = 'inventory-thumb'; + wrap.appendChild(img); + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.textContent = '×'; + removeBtn.title = 'Entfernen'; + removeBtn.style.cssText = 'position:absolute; top:-6px; right:-6px; width:18px; height:18px; border-radius:50%; border:1px solid var(--border); background:#fff; cursor:pointer; font-size:11px; line-height:1; padding:0;'; + removeBtn.onclick = () => { + inventoryPhotoDataUrls.splice(idx, 1); + renderInventoryPhotoPreview(); + }; + wrap.appendChild(removeBtn); + preview.appendChild(wrap); + }); + } + + function initInventoryForm() { + populateInventoryRoomSelect(); + populateInventoryConditionSelect(); + inventoryPhotoDataUrls = []; + renderInventoryPhotoPreview(); + + const photoInput = document.getElementById('inventoryPhotos'); + photoInput.onchange = async () => { + const files = Array.from(photoInput.files || []).slice(0, MAX_INVENTORY_PHOTOS - inventoryPhotoDataUrls.length); + for (const file of files) { + try { + const dataUrl = await readFileAsDataUrl(file); + inventoryPhotoDataUrls.push(dataUrl); + } catch (err) { + console.error('[inventory] Foto konnte nicht gelesen werden:', err); + } + } + photoInput.value = ''; + renderInventoryPhotoPreview(); + }; + + const form = document.getElementById('inventoryForm'); + const errorBox = document.getElementById('inventoryFormError'); + form.onsubmit = async (e) => { + e.preventDefault(); + errorBox.style.display = 'none'; + const submitBtn = document.getElementById('inventorySubmitBtn'); + submitBtn.disabled = true; + submitBtn.textContent = 'Speichert…'; + try { + const itemName = document.getElementById('inventoryItemName').value.trim(); + const roomId = document.getElementById('inventoryRoom').value || undefined; + const condition = document.getElementById('inventoryCondition').value; + const purchasePriceRaw = document.getElementById('inventoryPrice').value; + const purchasePrice = purchasePriceRaw ? Number(purchasePriceRaw) : undefined; + await apiFetch('/inventory', { + method: 'POST', + body: JSON.stringify({ itemName, roomId, condition, purchasePrice, photoUrls: inventoryPhotoDataUrls }), + }); + form.reset(); + populateInventoryConditionSelect(); + inventoryPhotoDataUrls = []; + renderInventoryPhotoPreview(); + loadInventory(); + } catch (err) { + errorBox.textContent = err.message; + errorBox.style.display = 'block'; + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Hinzufügen'; + } + }; + } + + async function loadInventory() { + if (!currentUser) return; + try { + const data = await apiFetch('/inventory'); + renderInventory(data.items); + } catch (err) { + console.error('[inventory] Laden fehlgeschlagen:', err.message); + } + } + + function renderInventory(items) { + const list = document.getElementById('inventoryList'); + list.innerHTML = ''; + if (!items.length) { + list.appendChild(el('div', 'empty-state', 'Noch kein Inventar erfasst.')); + return; + } + const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN'; + items.forEach(i => { + const row = el('div', 'ticket-row'); + const left = el('div'); + left.appendChild(el('div', 'title', i.itemName)); + left.appendChild(el('div', 'meta-text', + `${i.room ? i.room.roomNumber : 'Gemeinschaftsfläche'} · ${ITEM_CONDITION_LABEL[i.condition] || i.condition}` + + (i.purchasePrice ? ` · ${formatEuro(i.purchasePrice)}` : ''))); + if (i.photoUrls && i.photoUrls.length) { + const thumbRow = el('div', 'inventory-thumb-row'); + i.photoUrls.forEach(url => { + const img = document.createElement('img'); + img.src = url; + img.className = 'inventory-thumb'; + thumbRow.appendChild(img); + }); + left.appendChild(thumbRow); + } + row.appendChild(left); + if (isLandlord) { + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = 'Löschen'; + btn.onclick = () => deleteInventoryItem(i.id); + row.appendChild(btn); + } + list.appendChild(row); + }); + } + + async function deleteInventoryItem(id) { + try { + await apiFetch(`/inventory/${id}`, { method: 'DELETE' }); + loadInventory(); + } catch (err) { + alert(err.message); + } + } + + // --------------------------------------------------------------------- + // ÜBERGABEPROTOKOLLE + // --------------------------------------------------------------------- + + async function populateHandoverContractSelect() { + const select = document.getElementById('handoverContract'); + select.innerHTML = ''; + try { + const data = await apiFetch('/contracts'); + data.contracts.forEach(c => { + const opt = document.createElement('option'); + opt.value = c.id; + opt.textContent = `${c.user.fullName} · ${c.room.roomNumber}`; + select.appendChild(opt); + }); + } catch (err) { + console.error('[handover] Verträge laden fehlgeschlagen:', err.message); + } + } + + function initHandoverForm() { + populateHandoverContractSelect(); + const form = document.getElementById('handoverForm'); + const errorBox = document.getElementById('handoverFormError'); + form.onsubmit = async (e) => { + e.preventDefault(); + errorBox.style.display = 'none'; + const submitBtn = document.getElementById('handoverSubmitBtn'); + submitBtn.disabled = true; + submitBtn.textContent = 'Speichert…'; + try { + const contractId = document.getElementById('handoverContract').value; + const type = document.getElementById('handoverType').value; + const protocolDate = document.getElementById('handoverDate').value; + await apiFetch('/handover-protocols', { + method: 'POST', + body: JSON.stringify({ contractId, type, protocolDate, items: [], meterReadings: [] }), + }); + form.reset(); + loadHandoverProtocols(); + } catch (err) { + errorBox.textContent = err.message; + errorBox.style.display = 'block'; + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Protokoll anlegen'; + } + }; + } + + async function loadHandoverProtocols() { + if (!currentUser) return; + try { + const data = await apiFetch('/handover-protocols'); + renderHandoverProtocols(data.protocols); + } catch (err) { + console.error('[handover] Laden fehlgeschlagen:', err.message); + } + } + + function renderHandoverProtocols(protocols) { + const list = document.getElementById('handoverList'); + list.innerHTML = ''; + if (!protocols.length) { + list.appendChild(el('div', 'empty-state', 'Noch keine Übergabeprotokolle vorhanden.')); + return; + } + protocols.forEach(p => { + const row = el('div', 'ticket-row'); + const left = el('div'); + left.appendChild(el('div', 'title', `${HANDOVER_TYPE_LABEL[p.type] || p.type} · ${p.contract.room.roomNumber}`)); + left.appendChild(el('div', 'meta-text', + `${p.user.fullName} · ${new Date(p.protocolDate).toLocaleDateString('de-DE')} · ${p.items.length} Positionen, ${p.meterReadings.length} Zählerstände`)); + row.appendChild(left); + list.appendChild(row); + }); + } + + // --------------------------------------------------------------------- + // SMART-LOCK-GASTCODES + // --------------------------------------------------------------------- + + function initGuestCodeForm() { + const form = document.getElementById('guestCodeForm'); + const errorBox = document.getElementById('guestCodeFormError'); + form.onsubmit = async (e) => { + e.preventDefault(); + errorBox.style.display = 'none'; + const submitBtn = document.getElementById('guestCodeSubmitBtn'); + submitBtn.disabled = true; + submitBtn.textContent = 'Generiert…'; + try { + const validFrom = document.getElementById('guestCodeFrom').value; + const validUntil = document.getElementById('guestCodeUntil').value; + await apiFetch('/guest-codes', { method: 'POST', body: JSON.stringify({ validFrom, validUntil }) }); + form.reset(); + loadGuestCodes(); + } catch (err) { + errorBox.textContent = err.message; + errorBox.style.display = 'block'; + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Code generieren'; + } + }; + } + + async function loadGuestCodes() { + if (!currentUser) return; + try { + const data = await apiFetch('/guest-codes'); + renderGuestCodes(data.guestCodes); + } catch (err) { + console.error('[guest-codes] Laden fehlgeschlagen:', err.message); + } + } + + function renderGuestCodes(codes) { + const list = document.getElementById('guestCodeList'); + list.innerHTML = ''; + if (!codes.length) { + list.appendChild(el('div', 'empty-state', 'Noch keine Gästecodes ausgestellt.')); + return; + } + codes.forEach(g => { + const row = el('div', 'ticket-row'); + const left = el('div'); + left.appendChild(el('div', 'title', `Code: ${g.code}`)); + left.appendChild(el('div', 'meta-text', + `${g.issuedBy.fullName} · ${new Date(g.validFrom).toLocaleString('de-DE')} – ${new Date(g.validUntil).toLocaleString('de-DE')} · ${GUEST_CODE_STATUS_LABEL[g.status] || g.status}`)); + row.appendChild(left); + if (g.status === 'ACTIVE') { + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = 'Widerrufen'; + btn.onclick = () => revokeGuestCode(g.id); + row.appendChild(btn); + } + list.appendChild(row); + }); + } + + async function revokeGuestCode(id) { + try { + await apiFetch(`/guest-codes/${id}`, { method: 'DELETE' }); + loadGuestCodes(); + } catch (err) { + alert(err.message); + } + } + + // --------------------------------------------------------------------- + // KÜCHEN-/SCHRANKPLANER & MÜLL-KALENDER + // --------------------------------------------------------------------- + + function populateStorageSelects() { + const locSelect = document.getElementById('storageLocation'); + locSelect.innerHTML = ''; + Object.keys(STORAGE_LOCATION_LABEL).forEach(key => { + const opt = document.createElement('option'); + opt.value = key; + opt.textContent = STORAGE_LOCATION_LABEL[key]; + locSelect.appendChild(opt); + }); + + const trashSelect = document.getElementById('trashType'); + trashSelect.innerHTML = ''; + Object.keys(TRASH_TYPE_LABEL).forEach(key => { + const opt = document.createElement('option'); + opt.value = key; + opt.textContent = TRASH_TYPE_LABEL[key]; + trashSelect.appendChild(opt); + }); + } + + function initStorageForms() { + populateStorageSelects(); + const form = document.getElementById('storageForm'); + const errorBox = document.getElementById('storageFormError'); + form.onsubmit = async (e) => { + e.preventDefault(); + errorBox.style.display = 'none'; + const submitBtn = document.getElementById('storageSubmitBtn'); + submitBtn.disabled = true; + submitBtn.textContent = 'Speichert…'; + try { + const location = document.getElementById('storageLocation').value; + const label = document.getElementById('storageLabel').value.trim(); + await apiFetch('/storage-slots', { method: 'POST', body: JSON.stringify({ location, label }) }); + form.reset(); + loadStorage(); + } catch (err) { + errorBox.textContent = err.message; + errorBox.style.display = 'block'; + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Fach anlegen'; + } + }; + + const trashForm = document.getElementById('trashForm'); + const trashErrorBox = document.getElementById('trashFormError'); + trashForm.onsubmit = async (e) => { + e.preventDefault(); + trashErrorBox.style.display = 'none'; + const trashSubmitBtn = document.getElementById('trashSubmitBtn'); + trashSubmitBtn.disabled = true; + trashSubmitBtn.textContent = 'Speichert…'; + try { + const type = document.getElementById('trashType').value; + const date = document.getElementById('trashDate').value; + await apiFetch('/trash-schedule', { method: 'POST', body: JSON.stringify({ type, date }) }); + trashForm.reset(); + loadStorage(); + } catch (err) { + trashErrorBox.textContent = err.message; + trashErrorBox.style.display = 'block'; + } finally { + trashSubmitBtn.disabled = false; + trashSubmitBtn.textContent = 'Termin eintragen'; + } + }; + } + + async function loadStorage() { + if (!currentUser) return; + try { + const [slotsData, trashData] = await Promise.all([ + apiFetch('/storage-slots'), + apiFetch('/trash-schedule'), + ]); + renderStorageSlots(slotsData.slots); + renderTrashSchedule(trashData.entries); + } catch (err) { + console.error('[storage] Laden fehlgeschlagen:', err.message); + } + } + + function renderStorageSlots(slots) { + const list = document.getElementById('storageList'); + list.innerHTML = ''; + if (!slots.length) { + list.appendChild(el('div', 'empty-state', 'Noch keine Fächer angelegt.')); + return; + } + const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN'; + slots.forEach(s => { + const row = el('div', 'ticket-row'); + const left = el('div'); + left.appendChild(el('div', 'title', `${STORAGE_LOCATION_LABEL[s.location] || s.location} · ${s.label}`)); + left.appendChild(el('div', 'meta-text', s.user ? `Beansprucht von ${s.user.fullName}` : 'Frei')); + row.appendChild(left); + + const right = el('div'); + right.style.display = 'flex'; + right.style.gap = '8px'; + if (!s.user) { + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = 'Beanspruchen'; + btn.onclick = () => claimStorageSlot(s.id); + right.appendChild(btn); + } else if (isLandlord || s.user.id === currentUser.id) { + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = 'Freigeben'; + btn.onclick = () => releaseStorageSlot(s.id); + right.appendChild(btn); + } + if (isLandlord) { + const delBtn = document.createElement('button'); + delBtn.className = 'btn-secondary'; + delBtn.textContent = 'Löschen'; + delBtn.onclick = () => deleteStorageSlot(s.id); + right.appendChild(delBtn); + } + row.appendChild(right); + list.appendChild(row); + }); + } + + async function claimStorageSlot(id) { + try { + await apiFetch(`/storage-slots/${id}`, { method: 'PATCH', body: JSON.stringify({ userId: currentUser.id }) }); + loadStorage(); + } catch (err) { + alert(err.message); + } + } + + async function releaseStorageSlot(id) { + try { + await apiFetch(`/storage-slots/${id}`, { method: 'PATCH', body: JSON.stringify({ userId: null }) }); + loadStorage(); + } catch (err) { + alert(err.message); + } + } + + async function deleteStorageSlot(id) { + try { + await apiFetch(`/storage-slots/${id}`, { method: 'DELETE' }); + loadStorage(); + } catch (err) { + alert(err.message); + } + } + + function renderTrashSchedule(entries) { + const list = document.getElementById('trashList'); + list.innerHTML = ''; + const now = stripTime(new Date()); + const upcoming = entries.filter(e => stripTime(new Date(e.date)) >= now); + if (!upcoming.length) { + list.appendChild(el('div', 'empty-state', 'Keine anstehenden Mülltermine.')); + return; + } + const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN'; + upcoming.forEach(t => { + const row = el('div', 'ticket-row'); + const left = el('div'); + left.appendChild(el('div', 'title', TRASH_TYPE_LABEL[t.type] || t.type)); + left.appendChild(el('div', 'meta-text', new Date(t.date).toLocaleDateString('de-DE'))); + row.appendChild(left); + if (isLandlord) { + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = 'Löschen'; + btn.onclick = () => deleteTrashEntry(t.id); + row.appendChild(btn); + } + list.appendChild(row); + }); + } + + async function deleteTrashEntry(id) { + try { + await apiFetch(`/trash-schedule/${id}`, { method: 'DELETE' }); + loadStorage(); + } catch (err) { + alert(err.message); + } + } + + // --------------------------------------------------------------------- + // DOKUMENTEN-SAFE & TUTORIALS + // --------------------------------------------------------------------- + + function populateDocumentCategorySelect() { + const select = document.getElementById('documentCategory'); + select.innerHTML = ''; + Object.keys(DOCUMENT_CATEGORY_LABEL).forEach(key => { + const opt = document.createElement('option'); + opt.value = key; + opt.textContent = DOCUMENT_CATEGORY_LABEL[key]; + select.appendChild(opt); + }); + } + + function initDocumentForm() { + populateDocumentCategorySelect(); + const form = document.getElementById('documentForm'); + const errorBox = document.getElementById('documentFormError'); + form.onsubmit = async (e) => { + e.preventDefault(); + errorBox.style.display = 'none'; + const submitBtn = document.getElementById('documentSubmitBtn'); + submitBtn.disabled = true; + submitBtn.textContent = 'Speichert…'; + try { + const title = document.getElementById('documentTitle').value.trim(); + const category = document.getElementById('documentCategory').value; + const url = document.getElementById('documentUrl').value.trim(); + await apiFetch('/documents', { method: 'POST', body: JSON.stringify({ title, category, url }) }); + form.reset(); + populateDocumentCategorySelect(); + loadDocuments(); + } catch (err) { + errorBox.textContent = err.message; + errorBox.style.display = 'block'; + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Hinzufügen'; + } + }; + } + + async function loadDocuments() { + if (!currentUser) return; + try { + const data = await apiFetch('/documents'); + renderDocuments(data.documents); + } catch (err) { + console.error('[documents] Laden fehlgeschlagen:', err.message); + } + } + + function renderDocuments(documents) { + const list = document.getElementById('documentList'); + list.innerHTML = ''; + if (!documents.length) { + list.appendChild(el('div', 'empty-state', 'Noch keine Dokumente hinterlegt.')); + return; + } + const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN'; + documents.forEach(d => { + const row = el('div', 'ticket-row'); + const left = el('div'); + const link = document.createElement('a'); + link.href = d.url; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + link.textContent = d.title; + link.style.fontWeight = '600'; + link.style.color = 'var(--text)'; + left.appendChild(link); + left.appendChild(el('div', 'meta-text', `${DOCUMENT_CATEGORY_LABEL[d.category] || d.category} · ${d.uploadedBy.fullName}`)); + row.appendChild(left); + if (isLandlord) { + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = 'Löschen'; + btn.onclick = () => deleteDocument(d.id); + row.appendChild(btn); + } + list.appendChild(row); + }); + } + + async function deleteDocument(id) { + try { + await apiFetch(`/documents/${id}`, { method: 'DELETE' }); + loadDocuments(); + } catch (err) { + alert(err.message); + } + } + bootstrap();