From e6b16ea74e0d33b915c6c0a92a18a810f2189b68 Mon Sep 17 00:00:00 2001 From: Giuseppe Lombardo Date: Thu, 13 Aug 2026 17:14:47 +0000 Subject: [PATCH] Backend: Datumsbewusste Zimmerverfuegbarkeits-Pruefung (Plausibilitaetsabfrage) fuer Vertragsneuanla --- backend/src/routes/contracts.ts | 103 ++++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/backend/src/routes/contracts.ts b/backend/src/routes/contracts.ts index 60b9ade..c774dee 100644 --- a/backend/src/routes/contracts.ts +++ b/backend/src/routes/contracts.ts @@ -5,6 +5,31 @@ import { generateContractPdf } from '../services/contractDocumentGenerator'; const prisma = new PrismaClient(); +/** + * Prueft, ob auf einem Zimmer bereits ein aktiver Vertrag existiert, dessen + * Zeitraum sich mit [startDate, endDate] ueberschneidet. endDate=null steht + * fuer unbefristet (offenes Ende). Grundlage fuer die Plausibilitaetspruefung + * bei Neuvergabe/Einladung eines Zimmers. + */ +export async function findOverlappingContract( + client: PrismaClient, + roomId: string, + startDate: Date, + endDate: Date | null, +) { + const activeContracts = await client.contract.findMany({ + where: { roomId, isActive: true }, + }); + + return ( + activeContracts.find((c) => { + const otherEnd = c.endDate; + const overlaps = (!otherEnd || otherEnd >= startDate) && (!endDate || endDate >= c.startDate); + return overlaps; + }) || null + ); +} + /** * Router für Kündigungsfristen-Erinnerungen. * @@ -342,12 +367,66 @@ contractsRouter.get('/contracts/archive', requireAuth, requireRole('LANDLORD', ' // ---------------------------------------------------------------------------- // Zimmer ohne aktiven Vertrag — für die Auswahl beim Wiederaufnehmen. -contractsRouter.get('/contracts/vacant-rooms', requireAuth, requireRole('LANDLORD', 'ADMIN'), async (_req: AuthedRequest, res: Response) => { - const rooms = await prisma.room.findMany({ - where: { contracts: { none: { isActive: true } } }, +// Optional datumsbewusst: mit startDate/endDate werden auch Zimmer mit +// befristetem/gekuendigtem Vertrag beruecksichtigt, sofern deren Zeitraum +// nicht mit dem angefragten Zeitraum ueberlappt. Ohne Parameter bleibt das +// bisherige (rein isActive-basierte) Verhalten unveraendert. +contractsRouter.get('/contracts/vacant-rooms', requireAuth, requireRole('LANDLORD', 'ADMIN'), async (req: AuthedRequest, res: Response) => { + const { startDate, endDate } = req.query as { startDate?: string; endDate?: string }; + + if (!startDate || Number.isNaN(Date.parse(startDate))) { + const rooms = await prisma.room.findMany({ + where: { contracts: { none: { isActive: true } } }, + select: { id: true, roomNumber: true, sizeSqm: true, baseRent: true, utilityPauschal: true }, + orderBy: { roomNumber: 'asc' }, + }); + return res.status(200).json({ rooms }); + } + + const start = new Date(startDate); + const end = endDate && !Number.isNaN(Date.parse(endDate)) ? new Date(endDate) : null; + + const allRooms = await prisma.room.findMany({ select: { id: true, roomNumber: true, sizeSqm: true, baseRent: true, utilityPauschal: true }, orderBy: { roomNumber: 'asc' }, }); + + const rooms = []; + for (const room of allRooms) { + const conflict = await findOverlappingContract(prisma, room.id, start, end); + if (!conflict) rooms.push(room); + } + + res.status(200).json({ rooms }); +}); + +// Datumsbewusste Verfuegbarkeitspruefung ALLER Zimmer (auch belegte werden +// zurueckgegeben, aber mit available:false + Grund) — Basis fuer die +// Plausibilitaetspruefung im Frontend bei Einladung/Wiederaufnahme. +contractsRouter.get('/contracts/room-availability', requireAuth, requireRole('LANDLORD', 'ADMIN'), async (req: AuthedRequest, res: Response) => { + const { startDate, endDate } = req.query as { startDate?: string; endDate?: string }; + if (!startDate || Number.isNaN(Date.parse(startDate))) { + return res.status(400).json({ error: 'startDate ist erforderlich' }); + } + const start = new Date(startDate); + const end = endDate && !Number.isNaN(Date.parse(endDate)) ? new Date(endDate) : null; + + const allRooms = await prisma.room.findMany({ + select: { id: true, roomNumber: true, baseRent: true, utilityPauschal: true }, + orderBy: { roomNumber: 'asc' }, + }); + + const rooms = []; + for (const room of allRooms) { + const conflict = await findOverlappingContract(prisma, room.id, start, end); + const conflictUntil = conflict ? (conflict.endDate ? conflict.endDate.toLocaleDateString('de-DE') : 'unbefristet') : null; + rooms.push({ + ...room, + available: !conflict, + conflictReason: conflict ? `Zimmer ist im gewaehlten Zeitraum bereits vergeben (belegt bis ${conflictUntil}).` : undefined, + }); + } + res.status(200).json({ rooms }); }); @@ -360,11 +439,14 @@ contractsRouter.post( requireAuth, requireRole('LANDLORD', 'ADMIN'), async (req: AuthedRequest, res: Response) => { - const { userId, roomId, startDate, totalWarmRent, depositAmount, paymentDueDay, noticePeriodMonths } = req.body || {}; + const { userId, roomId, startDate, endDate, totalWarmRent, depositAmount, paymentDueDay, noticePeriodMonths } = req.body || {}; if (!userId || typeof userId !== 'string') return res.status(400).json({ error: 'userId ist erforderlich' }); if (!roomId || typeof roomId !== 'string') return res.status(400).json({ error: 'roomId ist erforderlich' }); if (!startDate || Number.isNaN(Date.parse(startDate))) return res.status(400).json({ error: 'startDate ist erforderlich' }); + if (endDate !== undefined && endDate !== null && Number.isNaN(Date.parse(endDate))) { + return res.status(400).json({ error: 'endDate ist ungueltig' }); + } if (!Number.isFinite(Number(totalWarmRent)) || Number(totalWarmRent) <= 0) { return res.status(400).json({ error: 'totalWarmRent muss eine positive Zahl sein' }); } @@ -385,12 +467,23 @@ contractsRouter.post( if (activeForUser) return res.status(409).json({ error: 'Mieter hat bereits einen aktiven Vertrag' }); if (activeForRoom) return res.status(409).json({ error: 'Zimmer ist bereits belegt' }); + const parsedStart = new Date(startDate); + const parsedEnd = endDate ? new Date(endDate) : null; + const overlap = await findOverlappingContract(prisma, roomId, parsedStart, parsedEnd); + if (overlap) { + const overlapUntil = overlap.endDate ? overlap.endDate.toLocaleDateString('de-DE') : 'unbefristet'; + return res.status(409).json({ + error: `Das Zimmer ist im gewählten Zeitraum bereits vergeben (belegt bis ${overlapUntil}).`, + }); + } + const [contract] = await prisma.$transaction([ prisma.contract.create({ data: { userId, roomId, - startDate: new Date(startDate), + startDate: parsedStart, + endDate: parsedEnd, totalWarmRent: Number(totalWarmRent), depositAmount: Number(depositAmount), paymentDueDay: Number.isFinite(Number(paymentDueDay)) ? Number(paymentDueDay) : 3,