Schaden melden: Foto als Pflichtfeld fuer Mieter, Vorschau in Ticket-Liste
This commit is contained in:
parent
d29f749b39
commit
6264bac99a
@ -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 } },
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -792,6 +792,11 @@
|
||||
<label for="ticketRoom">Bereich</label>
|
||||
<select id="ticketRoom"></select>
|
||||
</div>
|
||||
<div class="field" style="flex:1 1 100%;">
|
||||
<label for="ticketPhotos">Foto vom Schaden (bei Mietern Pflichtfeld)</label>
|
||||
<input type="file" id="ticketPhotos" accept="image/*" multiple />
|
||||
<div id="ticketPhotoPreview" style="display:flex; flex-wrap:wrap; gap:6px; margin-top:6px;"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" style="width:auto" id="ticketSubmitBtn">Melden</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user