From 9c99d3b631b1f06ac003afe5b08f2c27c31d87b7 Mon Sep 17 00:00:00 2001 From: Giuseppe Lombardo Date: Wed, 12 Aug 2026 19:37:04 +0000 Subject: [PATCH] Add cleaning-calendar and cleaning-absences endpoints --- backend/src/routes/cleaningTasks.ts | 118 ++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/backend/src/routes/cleaningTasks.ts b/backend/src/routes/cleaningTasks.ts index 0242fbd..c6df248 100644 --- a/backend/src/routes/cleaningTasks.ts +++ b/backend/src/routes/cleaningTasks.ts @@ -160,3 +160,121 @@ cleaningTasksRouter.patch( } }, ); + +// ---------------------------------------------------------------------------- +// KALENDERÜBERSICHT (Putz-Zuständigkeiten + Abwesenheiten) +// +// Wer wann für welchen Bereich zuständig ist, ist eine gemeinschaftliche +// Organisationsinfo und daher für ALLE eingeloggten Nutzer (auch Mieter) +// sichtbar — anders als z. B. Miethöhe oder private Zimmer-Tickets. +// ---------------------------------------------------------------------------- + +const ABSENCE_SELECT = { + id: true, + startDate: true, + endDate: true, + note: true, + user: { select: { id: true, fullName: true } }, +}; + +// GET /cleaning-calendar?from=YYYY-MM-DD&to=YYYY-MM-DD +// Liefert alle Putzaufgaben + alle Abwesenheiten im Zeitraum (für alle Rollen sichtbar). +cleaningTasksRouter.get( + '/cleaning-calendar', + requireAuth, + async (req: AuthedRequest, res: Response) => { + const { from, to } = req.query; + const fromDate = typeof from === 'string' && !Number.isNaN(Date.parse(from)) ? new Date(from) : undefined; + const toDate = typeof to === 'string' && !Number.isNaN(Date.parse(to)) ? new Date(to) : undefined; + + const taskWhere: Record = {}; + if (fromDate || toDate) { + taskWhere.dueDate = { + ...(fromDate ? { gte: fromDate } : {}), + ...(toDate ? { lte: toDate } : {}), + }; + } + + const absenceWhere: Record = {}; + if (fromDate || toDate) { + // Überlappung: Abwesenheit endet nicht vor "from" und beginnt nicht nach "to" + absenceWhere.AND = [ + fromDate ? { endDate: { gte: fromDate } } : {}, + toDate ? { startDate: { lte: toDate } } : {}, + ]; + } + + const [tasks, absences] = await Promise.all([ + prisma.cleaningTask.findMany({ + where: taskWhere, + orderBy: [{ dueDate: 'asc' }], + select: TASK_SELECT, + }), + prisma.cleaningAbsence.findMany({ + where: absenceWhere, + orderBy: [{ startDate: 'asc' }], + select: ABSENCE_SELECT, + }), + ]); + + res.status(200).json({ tasks, absences }); + }, +); + +// POST /cleaning-absences — Mieter trägt eigene Abwesenheit ein (Urlaub, "kann nicht" ...). +// Vermieter/Admin können Abwesenheiten für jeden Mieter anlegen (z. B. auf Zuruf). +cleaningTasksRouter.post( + '/cleaning-absences', + requireAuth, + async (req: AuthedRequest, res: Response) => { + const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN'; + const { startDate, endDate, note } = req.body || {}; + let { userId } = req.body || {}; + + if (!isLandlord) { + userId = req.user!.id; // Mieter dürfen nur für sich selbst eintragen + } else if (!userId || typeof userId !== 'string') { + return res.status(400).json({ error: 'userId ist erforderlich' }); + } + + if (!startDate || Number.isNaN(Date.parse(startDate))) { + return res.status(400).json({ error: 'startDate (Datum) ist erforderlich' }); + } + if (!endDate || Number.isNaN(Date.parse(endDate))) { + return res.status(400).json({ error: 'endDate (Datum) ist erforderlich' }); + } + if (new Date(endDate) < new Date(startDate)) { + return res.status(400).json({ error: 'endDate darf nicht vor startDate liegen' }); + } + + const absence = await prisma.cleaningAbsence.create({ + data: { + userId, + startDate: new Date(startDate), + endDate: new Date(endDate), + note: typeof note === 'string' && note.trim() ? note.trim().slice(0, 280) : null, + }, + select: ABSENCE_SELECT, + }); + + res.status(201).json({ absence }); + }, +); + +// DELETE /cleaning-absences/:id — eigener Eintrag löschbar, oder Vermieter/Admin jederzeit. +cleaningTasksRouter.delete( + '/cleaning-absences/:id', + requireAuth, + async (req: AuthedRequest, res: Response) => { + const absence = await prisma.cleaningAbsence.findUnique({ where: { id: req.params.id } }); + if (!absence) return res.status(404).json({ error: 'Abwesenheit nicht gefunden' }); + + const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN'; + if (!isLandlord && absence.userId !== req.user!.id) { + return res.status(403).json({ error: 'Du kannst nur deine eigenen Abwesenheiten löschen' }); + } + + await prisma.cleaningAbsence.delete({ where: { id: req.params.id } }); + res.status(204).send(); + }, +);