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) {