Backend: Datumsbewusste Zimmerverfuegbarkeits-Pruefung (Plausibilitaetsabfrage) fuer Vertragsneuanla
This commit is contained in:
parent
0d8401819a
commit
e6b16ea74e
@ -5,6 +5,31 @@ import { generateContractPdf } from '../services/contractDocumentGenerator';
|
|||||||
|
|
||||||
const prisma = new PrismaClient();
|
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.
|
* 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.
|
// Zimmer ohne aktiven Vertrag — für die Auswahl beim Wiederaufnehmen.
|
||||||
contractsRouter.get('/contracts/vacant-rooms', requireAuth, requireRole('LANDLORD', 'ADMIN'), async (_req: AuthedRequest, res: Response) => {
|
// Optional datumsbewusst: mit startDate/endDate werden auch Zimmer mit
|
||||||
const rooms = await prisma.room.findMany({
|
// befristetem/gekuendigtem Vertrag beruecksichtigt, sofern deren Zeitraum
|
||||||
where: { contracts: { none: { isActive: true } } },
|
// 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 },
|
select: { id: true, roomNumber: true, sizeSqm: true, baseRent: true, utilityPauschal: true },
|
||||||
orderBy: { roomNumber: 'asc' },
|
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 });
|
res.status(200).json({ rooms });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -360,11 +439,14 @@ contractsRouter.post(
|
|||||||
requireAuth,
|
requireAuth,
|
||||||
requireRole('LANDLORD', 'ADMIN'),
|
requireRole('LANDLORD', 'ADMIN'),
|
||||||
async (req: AuthedRequest, res: Response) => {
|
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 (!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 (!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 (!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) {
|
if (!Number.isFinite(Number(totalWarmRent)) || Number(totalWarmRent) <= 0) {
|
||||||
return res.status(400).json({ error: 'totalWarmRent muss eine positive Zahl sein' });
|
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 (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' });
|
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([
|
const [contract] = await prisma.$transaction([
|
||||||
prisma.contract.create({
|
prisma.contract.create({
|
||||||
data: {
|
data: {
|
||||||
userId,
|
userId,
|
||||||
roomId,
|
roomId,
|
||||||
startDate: new Date(startDate),
|
startDate: parsedStart,
|
||||||
|
endDate: parsedEnd,
|
||||||
totalWarmRent: Number(totalWarmRent),
|
totalWarmRent: Number(totalWarmRent),
|
||||||
depositAmount: Number(depositAmount),
|
depositAmount: Number(depositAmount),
|
||||||
paymentDueDay: Number.isFinite(Number(paymentDueDay)) ? Number(paymentDueDay) : 3,
|
paymentDueDay: Number.isFinite(Number(paymentDueDay)) ? Number(paymentDueDay) : 3,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user