From 57ee85292c0766e8ff756e114f3280b47de11279 Mon Sep 17 00:00:00 2001 From: Giuseppe Lombardo Date: Wed, 12 Aug 2026 18:50:23 +0000 Subject: [PATCH] Add Putzplan cleaning-tasks route --- backend/src/routes/cleaningTasks.ts | 147 ++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 backend/src/routes/cleaningTasks.ts diff --git a/backend/src/routes/cleaningTasks.ts b/backend/src/routes/cleaningTasks.ts new file mode 100644 index 0000000..bb9ffb7 --- /dev/null +++ b/backend/src/routes/cleaningTasks.ts @@ -0,0 +1,147 @@ +import { Router, Response } from 'express'; +import { PrismaClient, CleaningArea, CleaningStatus } from '@prisma/client'; +import { AuthedRequest, requireAuth, requireRole } from '../middleware/auth'; + +const prisma = new PrismaClient(); + +/** + * Router für den Putzplan (rotierende Reinigungsaufgaben). + * + * Zugriffsregel: ein Mieter sieht (und kann abhaken) nur seine eigenen + * zugewiesenen Putzaufgaben. Vermieter/Admin sehen und verwalten den + * kompletten Putzplan aller Mieter. + */ +export const cleaningTasksRouter = Router(); + +const AREAS = Object.values(CleaningArea); + +const TASK_SELECT = { + id: true, + area: true, + weekOf: true, + dueDate: true, + completedAt: true, + proofImageUrl: true, + status: true, + verifiedBy: true, + assignedUser: { select: { id: true, fullName: true } }, +}; + +// Vermieter/Admin legen eine neue Putzaufgabe an (z. B. für die kommende Rotationswoche). +cleaningTasksRouter.post( + '/cleaning-tasks', + requireAuth, + requireRole('LANDLORD', 'ADMIN'), + async (req: AuthedRequest, res: Response) => { + const { assignedUserId, area, weekOf, dueDate } = req.body || {}; + + if (!assignedUserId || typeof assignedUserId !== 'string') { + return res.status(400).json({ error: 'assignedUserId ist erforderlich' }); + } + if (!area || !AREAS.includes(area)) { + return res.status(400).json({ error: `Ungültiger Bereich. Erlaubt: ${AREAS.join(', ')}` }); + } + if (!weekOf || Number.isNaN(Date.parse(weekOf))) { + return res.status(400).json({ error: 'weekOf (Datum) ist erforderlich' }); + } + if (!dueDate || Number.isNaN(Date.parse(dueDate))) { + return res.status(400).json({ error: 'dueDate (Datum) ist erforderlich' }); + } + + const assignedUser = await prisma.user.findUnique({ where: { id: assignedUserId } }); + if (!assignedUser) { + return res.status(400).json({ error: 'Zugewiesener Nutzer nicht gefunden' }); + } + + const task = await prisma.cleaningTask.create({ + data: { + assignedUserId, + area, + weekOf: new Date(weekOf), + dueDate: new Date(dueDate), + status: CleaningStatus.PENDING, + }, + select: TASK_SELECT, + }); + + res.status(201).json({ task }); + }, +); + +cleaningTasksRouter.get('/cleaning-tasks', requireAuth, async (req: AuthedRequest, res: Response) => { + const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN'; + const statusFilter = typeof req.query.status === 'string' ? req.query.status : undefined; + + const where: Record = {}; + if (statusFilter && (Object.values(CleaningStatus) as string[]).includes(statusFilter)) { + where.status = statusFilter; + } + if (!isLandlord) { + where.assignedUserId = req.user!.id; + } + + const tasks = await prisma.cleaningTask.findMany({ + where, + orderBy: [{ weekOf: 'desc' }, { dueDate: 'asc' }], + select: TASK_SELECT, + }); + + res.status(200).json({ tasks }); +}); + +// Mieter hakt die eigene Putzaufgabe ab, optional mit Fotobeweis (URL). +// Vermieter/Admin dürfen ebenfalls jede Aufgabe abschließen (z. B. Vertretung). +cleaningTasksRouter.post( + '/cleaning-tasks/:id/complete', + requireAuth, + async (req: AuthedRequest, res: Response) => { + const task = await prisma.cleaningTask.findUnique({ where: { id: req.params.id } }); + if (!task) return res.status(404).json({ error: 'Putzaufgabe nicht gefunden' }); + + const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN'; + if (!isLandlord && task.assignedUserId !== req.user!.id) { + return res.status(403).json({ error: 'Du kannst nur deine eigenen Putzaufgaben abhaken' }); + } + + const { proofImageUrl } = req.body || {}; + + const updated = await prisma.cleaningTask.update({ + where: { id: req.params.id }, + data: { + status: CleaningStatus.COMPLETED, + completedAt: new Date(), + proofImageUrl: typeof proofImageUrl === 'string' && proofImageUrl.trim() ? proofImageUrl.trim() : task.proofImageUrl, + }, + select: TASK_SELECT, + }); + + res.status(200).json({ task: updated }); + }, +); + +// Vermieter/Admin bestätigt eine erledigte Aufgabe (Mitbewohner-Verifizierung) oder markiert sie als verpasst. +cleaningTasksRouter.patch( + '/cleaning-tasks/:id', + requireAuth, + requireRole('LANDLORD', 'ADMIN'), + async (req: AuthedRequest, res: Response) => { + const { status } = req.body || {}; + if (!status || !(Object.values(CleaningStatus) as string[]).includes(status)) { + return res.status(400).json({ error: `Ungültiger Status. Erlaubt: ${Object.values(CleaningStatus).join(', ')}` }); + } + + try { + const task = await prisma.cleaningTask.update({ + where: { id: req.params.id }, + data: { + status, + verifiedBy: status === CleaningStatus.VERIFIED ? req.user!.id : undefined, + }, + select: TASK_SELECT, + }); + res.status(200).json({ task }); + } catch { + res.status(404).json({ error: 'Putzaufgabe nicht gefunden' }); + } + }, +);