diff --git a/web-dashboard/index.html b/web-dashboard/index.html index 43a2c6d..d0aa627 100644 --- a/web-dashboard/index.html +++ b/web-dashboard/index.html @@ -388,11 +388,41 @@ -
-

Miet-Ampel je Zimmer

+
+

Miet-Ampel je Zimmer

+
+

Schaden melden

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

Offene Schadensmeldungen

@@ -409,6 +439,11 @@ const PRIORITY_LABEL = { LOW: 'Niedrig', MEDIUM: 'Mittel', HIGH: 'Hoch', EMERGENCY: 'Notfall' }; const ROLE_LABEL = { LANDLORD: 'Vermieter', TENANT: 'Mieter', CRAFTSMAN: 'Handwerker', ADMIN: 'Verwaltung' }; const INVITE_STATUS_LABEL = { PENDING: 'Offen', ACCEPTED: 'Angenommen', EXPIRED: 'Abgelaufen', REVOKED: 'Zurückgezogen' }; + const CATEGORY_LABEL = { + SANITAER: 'Sanitär', ELEKTRIK: 'Elektrik', MOEBEL: 'Möbel', HEIZUNG: 'Heizung', + SCHIMMEL: 'Schimmel', SCHLUESSEL_SCHLOSS: 'Schlüssel/Schloss', SONSTIGES: 'Sonstiges', + }; + const TICKET_STATUS_LABEL = { OPEN: 'Gemeldet', IN_PROGRESS: 'In Bearbeitung', RESOLVED: 'Erledigt', CLOSED: 'Geschlossen' }; function formatEuro(n) { return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(n || 0); @@ -573,8 +608,10 @@ const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN'; document.getElementById('inviteSection').style.display = isLandlord ? 'block' : 'none'; + document.getElementById('rentSectionTitle').textContent = isLandlord ? 'Miet-Ampel je Zimmer' : 'Meine Miete'; if (isLandlord) initInvitePanel(); + initTicketForm(); loadData(); setInterval(loadData, REFRESH_INTERVAL_MS); } @@ -687,7 +724,9 @@ try { const data = await apiFetch('/cockpit/summary'); lastRooms = data.rooms || []; + lastMyRoom = data.myRoom || null; populateRoomSelect(); + populateTicketRoomSelect(); render(data); } catch (err) { if (err.message === 'Nicht authentifiziert' || err.message === 'Token ungültig oder abgelaufen') { @@ -704,16 +743,28 @@ document.getElementById('lastUpdated').textContent = 'Stand: ' + new Date(data.generatedAt).toLocaleTimeString('de-DE'); + const isLandlord = data.role === 'LANDLORD' || data.role === 'ADMIN'; + // --- Stat-Kacheln --- const statRow = document.getElementById('statRow'); statRow.innerHTML = ''; - const stats = [ - { label: 'Bezahlt', value: data.summary.rentGreen, cls: 'green' }, - { label: 'Fällig', value: data.summary.rentYellow, cls: 'yellow' }, - { label: 'Überfällig', value: data.summary.rentRed, cls: 'red' }, - { label: 'Soll-Miete gesamt / Monat', value: formatEuro(data.summary.totalMonthlyRentTarget), cls: '' }, - { label: 'Offene Tickets', value: data.summary.openTicketsCount, cls: '' }, - ]; + let stats; + if (isLandlord) { + stats = [ + { label: 'Bezahlt', value: data.summary.rentGreen, cls: 'green' }, + { label: 'Fällig', value: data.summary.rentYellow, cls: 'yellow' }, + { label: 'Überfällig', value: data.summary.rentRed, cls: 'red' }, + { label: 'Soll-Miete gesamt / Monat', value: formatEuro(data.summary.totalMonthlyRentTarget), cls: '' }, + { label: 'Offene Tickets', value: data.summary.openTicketsCount, cls: '' }, + ]; + } else { + const myStatusCls = data.myRent ? { GREEN: 'green', YELLOW: 'yellow', RED: 'red' }[data.myRent.status] : ''; + stats = [ + { label: 'Mein Mietstatus', value: data.myRent ? STATUS_LABEL[data.myRent.status] : '–', cls: myStatusCls }, + { label: 'Fällig', value: formatEuro(data.myRent ? data.myRent.amountDue : 0), cls: '' }, + { label: 'Offene Meldungen', value: data.openTickets.length, cls: '' }, + ]; + } stats.forEach(s => { const tile = el('div', `stat-tile ${s.cls}`); tile.appendChild(el('div', 'value', s.value)); @@ -721,19 +772,21 @@ statRow.appendChild(tile); }); - // --- Zimmer-Ampeln --- + // --- Miet-Ampel(n) --- const roomGrid = document.getElementById('roomGrid'); roomGrid.innerHTML = ''; - if (!data.trafficLights.length) { - roomGrid.appendChild(el('div', 'empty-state', 'Keine aktiven Verträge gefunden.')); + const lights = isLandlord ? data.trafficLights : (data.myRent ? [data.myRent] : []); + if (!lights.length) { + roomGrid.appendChild(el('div', 'empty-state', + isLandlord ? 'Keine aktiven Verträge gefunden.' : 'Kein aktiver Mietvertrag gefunden.')); } - data.trafficLights.forEach(light => { + lights.forEach(light => { const card = el('div', `room-card ${light.status}`); const top = el('div', 'room-card-top'); top.appendChild(el('div', 'room-name', light.roomNumber)); top.appendChild(el('div', `badge ${light.status}`, STATUS_LABEL[light.status])); card.appendChild(top); - card.appendChild(el('div', 'tenant', light.tenantName)); + if (isLandlord) card.appendChild(el('div', 'tenant', light.tenantName)); const amountRow = el('div', 'amount-row'); amountRow.appendChild(el('span', 'label', 'Fällig')); @@ -760,13 +813,148 @@ const left = el('div'); left.appendChild(el('div', 'title', t.title)); left.appendChild(el('div', 'meta-text', - `${t.room ? t.room.roomNumber : 'Gemeinschaftsbereich'} · ${new Date(t.createdAt).toLocaleDateString('de-DE')}`)); + `${t.room ? t.room.roomNumber : 'Gemeinschaftsbereich'} · ${CATEGORY_LABEL[t.category] || t.category} · ` + + `${t.creator ? t.creator.fullName + ' · ' : ''}${new Date(t.createdAt).toLocaleDateString('de-DE')} · ` + + TICKET_STATUS_LABEL[t.status])); row.appendChild(left); - row.appendChild(el('span', `priority-chip ${t.priority}`, PRIORITY_LABEL[t.priority] || t.priority)); + + const right = el('div'); + right.style.display = 'flex'; + right.style.gap = '8px'; + right.style.alignItems = 'center'; + right.appendChild(el('span', `priority-chip ${t.priority}`, PRIORITY_LABEL[t.priority] || t.priority)); + + if (isLandlord) { + if (t.status === 'OPEN') { + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = 'In Bearbeitung'; + btn.onclick = () => updateTicketStatus(t.id, 'IN_PROGRESS'); + right.appendChild(btn); + } + if (t.status === 'OPEN' || t.status === 'IN_PROGRESS') { + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = 'Erledigt'; + btn.onclick = () => updateTicketStatus(t.id, 'RESOLVED'); + right.appendChild(btn); + } + } else if (t.creator && t.creator.id === currentUser.id && t.status === 'OPEN') { + const btn = document.createElement('button'); + btn.className = 'btn-secondary'; + btn.textContent = 'Zurückziehen'; + btn.onclick = () => withdrawTicket(t.id); + right.appendChild(btn); + } + + row.appendChild(right); ticketList.appendChild(row); }); } + // --------------------------------------------------------------------- + // SCHADENSMELDUNGEN / TICKETS + // --------------------------------------------------------------------- + + let lastMyRoom = null; + + function populateTicketRoomSelect() { + const select = document.getElementById('ticketRoom'); + if (!select) return; + select.innerHTML = ''; + const generalOpt = document.createElement('option'); + generalOpt.value = ''; + generalOpt.textContent = 'Gemeinschaftsbereich (Küche, Bad, Flur, ...)'; + select.appendChild(generalOpt); + + const isLandlord = currentUser && (currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN'); + if (isLandlord) { + lastRooms.forEach(r => { + const opt = document.createElement('option'); + opt.value = r.id; + opt.textContent = r.roomNumber; + select.appendChild(opt); + }); + } else if (lastMyRoom) { + const opt = document.createElement('option'); + opt.value = lastMyRoom.id; + opt.textContent = `Mein Zimmer (${lastMyRoom.roomNumber})`; + select.appendChild(opt); + } + } + + function populateTicketSelects() { + const catSelect = document.getElementById('ticketCategory'); + catSelect.innerHTML = ''; + Object.keys(CATEGORY_LABEL).forEach(key => { + const opt = document.createElement('option'); + opt.value = key; + opt.textContent = CATEGORY_LABEL[key]; + catSelect.appendChild(opt); + }); + + const prioSelect = document.getElementById('ticketPriority'); + prioSelect.innerHTML = ''; + Object.keys(PRIORITY_LABEL).forEach(key => { + const opt = document.createElement('option'); + opt.value = key; + opt.textContent = PRIORITY_LABEL[key]; + prioSelect.appendChild(opt); + }); + prioSelect.value = 'MEDIUM'; + } + + function initTicketForm() { + populateTicketSelects(); + const form = document.getElementById('ticketForm'); + const errorBox = document.getElementById('ticketFormError'); + form.onsubmit = async (e) => { + e.preventDefault(); + errorBox.style.display = 'none'; + const submitBtn = document.getElementById('ticketSubmitBtn'); + submitBtn.disabled = true; + submitBtn.textContent = 'Meldet…'; + try { + const title = document.getElementById('ticketTitle').value.trim(); + const description = document.getElementById('ticketDescription').value.trim(); + const category = document.getElementById('ticketCategory').value; + const priority = document.getElementById('ticketPriority').value; + const roomId = document.getElementById('ticketRoom').value || undefined; + await apiFetch('/tickets', { + method: 'POST', + body: JSON.stringify({ title, description, category, priority, roomId }), + }); + form.reset(); + populateTicketSelects(); + loadData(); + } catch (err) { + errorBox.textContent = err.message; + errorBox.style.display = 'block'; + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Melden'; + } + }; + } + + async function updateTicketStatus(id, status) { + try { + await apiFetch(`/tickets/${id}`, { method: 'PATCH', body: JSON.stringify({ status }) }); + loadData(); + } catch (err) { + alert(err.message); + } + } + + async function withdrawTicket(id) { + try { + await apiFetch(`/tickets/${id}`, { method: 'DELETE' }); + loadData(); + } catch (err) { + alert(err.message); + } + } + bootstrap();