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,
|
priority: true,
|
||||||
status: true,
|
status: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
imageUrls: true,
|
||||||
room: { select: { roomNumber: true } },
|
room: { select: { roomNumber: true } },
|
||||||
creator: { select: { id: true, fullName: true } },
|
creator: { select: { id: true, fullName: true } },
|
||||||
craftsman: { select: { id: true, name: true, trade: true, phone: 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) => {
|
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()) {
|
if (!title || typeof title !== 'string' || !title.trim()) {
|
||||||
return res.status(400).json({ error: 'Titel ist erforderlich' });
|
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 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;
|
let targetRoomId: string | null = roomId || null;
|
||||||
|
|
||||||
if (!isLandlord) {
|
if (!isLandlord) {
|
||||||
@ -79,6 +89,7 @@ ticketsRouter.post('/tickets', requireAuth, async (req: AuthedRequest, res: Resp
|
|||||||
category,
|
category,
|
||||||
priority: priority || TicketPriority.MEDIUM,
|
priority: priority || TicketPriority.MEDIUM,
|
||||||
status: TicketStatus.OPEN,
|
status: TicketStatus.OPEN,
|
||||||
|
imageUrls: sanitizedImageUrls,
|
||||||
},
|
},
|
||||||
select: TICKET_SELECT,
|
select: TICKET_SELECT,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -792,6 +792,11 @@
|
|||||||
<label for="ticketRoom">Bereich</label>
|
<label for="ticketRoom">Bereich</label>
|
||||||
<select id="ticketRoom"></select>
|
<select id="ticketRoom"></select>
|
||||||
</div>
|
</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>
|
<button type="submit" class="btn-primary" style="width:auto" id="ticketSubmitBtn">Melden</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -1891,6 +1896,19 @@
|
|||||||
if (t.craftsman) {
|
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 : ''}`));
|
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);
|
row.appendChild(left);
|
||||||
|
|
||||||
const right = el('div');
|
const right = el('div');
|
||||||
@ -2002,13 +2020,64 @@
|
|||||||
prioSelect.value = 'MEDIUM';
|
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() {
|
function initTicketForm() {
|
||||||
populateTicketSelects();
|
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 form = document.getElementById('ticketForm');
|
||||||
const errorBox = document.getElementById('ticketFormError');
|
const errorBox = document.getElementById('ticketFormError');
|
||||||
form.onsubmit = async (e) => {
|
form.onsubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
errorBox.style.display = 'none';
|
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');
|
const submitBtn = document.getElementById('ticketSubmitBtn');
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitBtn.textContent = 'Meldet…';
|
submitBtn.textContent = 'Meldet…';
|
||||||
@ -2020,9 +2089,11 @@
|
|||||||
const roomId = document.getElementById('ticketRoom').value || undefined;
|
const roomId = document.getElementById('ticketRoom').value || undefined;
|
||||||
await apiFetch('/tickets', {
|
await apiFetch('/tickets', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ title, description, category, priority, roomId }),
|
body: JSON.stringify({ title, description, category, priority, roomId, imageUrls: ticketPhotoDataUrls }),
|
||||||
});
|
});
|
||||||
form.reset();
|
form.reset();
|
||||||
|
ticketPhotoDataUrls = [];
|
||||||
|
renderTicketPhotoPreview();
|
||||||
populateTicketSelects();
|
populateTicketSelects();
|
||||||
loadData();
|
loadData();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user