From 6264bac99a38b264cf2926d268e2670cdba16cf7 Mon Sep 17 00:00:00 2001 From: bernd Date: Thu, 13 Aug 2026 19:32:54 +0000 Subject: [PATCH] Schaden melden: Foto als Pflichtfeld fuer Mieter, Vorschau in Ticket-Liste --- backend/src/routes/cockpit.ts | 1 + backend/src/routes/tickets.ts | 13 ++++++- web-dashboard/index.html | 73 ++++++++++++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/cockpit.ts b/backend/src/routes/cockpit.ts index e58b0bc..d70ec20 100644 --- a/backend/src/routes/cockpit.ts +++ b/backend/src/routes/cockpit.ts @@ -16,6 +16,7 @@ const TICKET_SELECT = { priority: true, status: true, createdAt: true, + imageUrls: true, room: { select: { roomNumber: true } }, creator: { select: { id: true, fullName: true } }, craftsman: { select: { id: true, name: true, trade: true, phone: true } }, diff --git a/backend/src/routes/tickets.ts b/backend/src/routes/tickets.ts index 5ce3303..0c1df5b 100644 --- a/backend/src/routes/tickets.ts +++ b/backend/src/routes/tickets.ts @@ -41,7 +41,7 @@ const TICKET_SELECT = { }; ticketsRouter.post('/tickets', requireAuth, async (req: AuthedRequest, res: Response) => { - const { title, description, category, priority, roomId } = req.body || {}; + const { title, description, category, priority, roomId, imageUrls } = req.body || {}; if (!title || typeof title !== 'string' || !title.trim()) { return res.status(400).json({ error: 'Titel ist erforderlich' }); @@ -57,6 +57,16 @@ ticketsRouter.post('/tickets', requireAuth, async (req: AuthedRequest, res: Resp } const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN'; + const sanitizedImageUrls = Array.isArray(imageUrls) + ? imageUrls.filter((u) => typeof u === 'string' && u.trim()).map((u) => u.trim()) + : []; + + // Mieter müssen mindestens ein Foto als Beleg hochladen — Vermieter/Admin + // (die z. B. auf Zuruf eines Mieters melden) sind davon ausgenommen. + if (!isLandlord && sanitizedImageUrls.length === 0) { + return res.status(400).json({ error: 'Bitte mindestens ein Foto zum Schaden hochladen' }); + } + let targetRoomId: string | null = roomId || null; if (!isLandlord) { @@ -79,6 +89,7 @@ ticketsRouter.post('/tickets', requireAuth, async (req: AuthedRequest, res: Resp category, priority: priority || TicketPriority.MEDIUM, status: TicketStatus.OPEN, + imageUrls: sanitizedImageUrls, }, select: TICKET_SELECT, }); diff --git a/web-dashboard/index.html b/web-dashboard/index.html index be03508..13d021e 100644 --- a/web-dashboard/index.html +++ b/web-dashboard/index.html @@ -792,6 +792,11 @@ +
+ + +
+
@@ -1891,6 +1896,19 @@ if (t.craftsman) { left.appendChild(el('div', 'meta-text', `Handwerker: ${t.craftsman.name} (${CRAFTSMAN_TRADE_LABEL[t.craftsman.trade] || t.craftsman.trade})${t.craftsman.phone ? ' · ' + t.craftsman.phone : ''}`)); } + if (t.imageUrls && t.imageUrls.length) { + const photoWrap = el('div'); + photoWrap.style.cssText = 'display:flex; flex-wrap:wrap; gap:6px; margin-top:6px;'; + t.imageUrls.forEach(url => { + const img = document.createElement('img'); + img.src = url; + img.className = 'inventory-thumb'; + img.style.cursor = 'pointer'; + img.onclick = () => openDocumentPreview(t.title, url); + photoWrap.appendChild(img); + }); + left.appendChild(photoWrap); + } row.appendChild(left); const right = el('div'); @@ -2002,13 +2020,64 @@ prioSelect.value = 'MEDIUM'; } + const MAX_TICKET_PHOTOS = 4; + let ticketPhotoDataUrls = []; + + function renderTicketPhotoPreview() { + const preview = document.getElementById('ticketPhotoPreview'); + preview.innerHTML = ''; + ticketPhotoDataUrls.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 = () => { + ticketPhotoDataUrls.splice(idx, 1); + renderTicketPhotoPreview(); + }; + wrap.appendChild(removeBtn); + preview.appendChild(wrap); + }); + } + function initTicketForm() { populateTicketSelects(); + ticketPhotoDataUrls = []; + renderTicketPhotoPreview(); + + const photoInput = document.getElementById('ticketPhotos'); + photoInput.onchange = async () => { + const files = Array.from(photoInput.files || []).slice(0, MAX_TICKET_PHOTOS - ticketPhotoDataUrls.length); + for (const file of files) { + try { + const dataUrl = await readFileAsCompressedDataUrl(file, 1600, 0.82); + ticketPhotoDataUrls.push(dataUrl); + } catch (err) { + console.error('[tickets] Foto konnte nicht gelesen werden:', err); + } + } + photoInput.value = ''; + renderTicketPhotoPreview(); + }; + const form = document.getElementById('ticketForm'); const errorBox = document.getElementById('ticketFormError'); form.onsubmit = async (e) => { e.preventDefault(); errorBox.style.display = 'none'; + const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN'; + if (!isLandlord && ticketPhotoDataUrls.length === 0) { + errorBox.textContent = 'Bitte mindestens ein Foto zum Schaden hochladen'; + errorBox.style.display = 'block'; + return; + } const submitBtn = document.getElementById('ticketSubmitBtn'); submitBtn.disabled = true; submitBtn.textContent = 'Meldet…'; @@ -2020,9 +2089,11 @@ const roomId = document.getElementById('ticketRoom').value || undefined; await apiFetch('/tickets', { method: 'POST', - body: JSON.stringify({ title, description, category, priority, roomId }), + body: JSON.stringify({ title, description, category, priority, roomId, imageUrls: ticketPhotoDataUrls }), }); form.reset(); + ticketPhotoDataUrls = []; + renderTicketPhotoPreview(); populateTicketSelects(); loadData(); } catch (err) {