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
+
+
+
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();