add car reservation feature
This commit is contained in:
+130
-11
@@ -112,18 +112,137 @@ router.get('/:id/availability', async (req, res, next) => {
|
||||
const { startDate, endDate } = req.query as Record<string, string>
|
||||
if (!startDate || !endDate) return res.status(400).json({ error: 'missing_params', message: 'startDate and endDate required', statusCode: 400 })
|
||||
|
||||
const conflicts = await prisma.reservation.findMany({
|
||||
where: {
|
||||
vehicleId: req.params.id,
|
||||
companyId: req.companyId,
|
||||
status: { in: ['CONFIRMED', 'ACTIVE'] },
|
||||
startDate: { lt: new Date(endDate) },
|
||||
endDate: { gt: new Date(startDate) },
|
||||
},
|
||||
select: { id: true, startDate: true, endDate: true, status: true },
|
||||
})
|
||||
const start = new Date(startDate)
|
||||
const end = new Date(endDate)
|
||||
|
||||
res.json({ data: { available: conflicts.length === 0, conflicts } })
|
||||
const [reservationConflicts, blockConflicts] = await Promise.all([
|
||||
prisma.reservation.findMany({
|
||||
where: {
|
||||
vehicleId: req.params.id,
|
||||
companyId: req.companyId,
|
||||
status: { in: ['CONFIRMED', 'ACTIVE'] },
|
||||
startDate: { lt: end },
|
||||
endDate: { gt: start },
|
||||
},
|
||||
select: { id: true, startDate: true, endDate: true, status: true },
|
||||
}),
|
||||
prisma.vehicleCalendarBlock.findMany({
|
||||
where: {
|
||||
vehicleId: req.params.id,
|
||||
startDate: { lt: end },
|
||||
endDate: { gt: start },
|
||||
},
|
||||
select: { id: true, startDate: true, endDate: true, type: true, reason: true },
|
||||
}),
|
||||
])
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
available: reservationConflicts.length === 0 && blockConflicts.length === 0,
|
||||
conflicts: reservationConflicts,
|
||||
blocks: blockConflicts,
|
||||
},
|
||||
})
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/:id/calendar', async (req, res, next) => {
|
||||
try {
|
||||
const year = parseInt(req.query.year as string)
|
||||
const month = parseInt(req.query.month as string)
|
||||
if (isNaN(year) || isNaN(month) || month < 1 || month > 12) {
|
||||
return res.status(400).json({ error: 'invalid_params', message: 'year and month (1-12) required', statusCode: 400 })
|
||||
}
|
||||
|
||||
// Validate vehicle belongs to company
|
||||
await prisma.vehicle.findFirstOrThrow({ where: { id: req.params.id, companyId: req.companyId } })
|
||||
|
||||
const rangeStart = new Date(year, month - 1, 1)
|
||||
const rangeEnd = new Date(year, month, 1)
|
||||
|
||||
const [reservations, blocks] = await Promise.all([
|
||||
prisma.reservation.findMany({
|
||||
where: {
|
||||
vehicleId: req.params.id,
|
||||
companyId: req.companyId,
|
||||
status: { in: ['DRAFT', 'CONFIRMED', 'ACTIVE'] },
|
||||
startDate: { lt: rangeEnd },
|
||||
endDate: { gt: rangeStart },
|
||||
},
|
||||
select: {
|
||||
id: true, startDate: true, endDate: true, status: true,
|
||||
customer: { select: { firstName: true, lastName: true } },
|
||||
},
|
||||
orderBy: { startDate: 'asc' },
|
||||
}),
|
||||
prisma.vehicleCalendarBlock.findMany({
|
||||
where: {
|
||||
vehicleId: req.params.id,
|
||||
startDate: { lt: rangeEnd },
|
||||
endDate: { gt: rangeStart },
|
||||
},
|
||||
orderBy: { startDate: 'asc' },
|
||||
}),
|
||||
])
|
||||
|
||||
const events = [
|
||||
...reservations.map((r) => ({
|
||||
id: r.id,
|
||||
type: 'RESERVATION' as const,
|
||||
startDate: r.startDate,
|
||||
endDate: r.endDate,
|
||||
status: r.status,
|
||||
label: r.customer ? `${r.customer.firstName} ${r.customer.lastName}` : 'Reserved',
|
||||
})),
|
||||
...blocks.map((b) => ({
|
||||
id: b.id,
|
||||
type: b.type === 'MAINTENANCE' ? 'MAINTENANCE' as const : 'BLOCK' as const,
|
||||
startDate: b.startDate,
|
||||
endDate: b.endDate,
|
||||
status: null,
|
||||
label: b.reason ?? (b.type === 'MAINTENANCE' ? 'Maintenance' : 'Blocked'),
|
||||
})),
|
||||
]
|
||||
|
||||
res.json({ data: events })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/:id/calendar/blocks', async (req, res, next) => {
|
||||
try {
|
||||
const body = z.object({
|
||||
startDate: z.string().datetime({ offset: true }).or(z.string().regex(/^\d{4}-\d{2}-\d{2}$/)),
|
||||
endDate: z.string().datetime({ offset: true }).or(z.string().regex(/^\d{4}-\d{2}-\d{2}$/)),
|
||||
reason: z.string().optional(),
|
||||
type: z.enum(['MANUAL', 'MAINTENANCE']).default('MANUAL'),
|
||||
}).parse(req.body)
|
||||
|
||||
const start = new Date(body.startDate)
|
||||
const end = new Date(body.endDate)
|
||||
if (end <= start) return res.status(400).json({ error: 'invalid_range', message: 'endDate must be after startDate', statusCode: 400 })
|
||||
|
||||
await prisma.vehicle.findFirstOrThrow({ where: { id: req.params.id, companyId: req.companyId } })
|
||||
|
||||
const block = await prisma.vehicleCalendarBlock.create({
|
||||
data: {
|
||||
vehicleId: req.params.id,
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
reason: body.reason,
|
||||
type: body.type,
|
||||
},
|
||||
})
|
||||
res.status(201).json({ data: block })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.delete('/:id/calendar/blocks/:blockId', async (req, res, next) => {
|
||||
try {
|
||||
await prisma.vehicle.findFirstOrThrow({ where: { id: req.params.id, companyId: req.companyId } })
|
||||
await prisma.vehicleCalendarBlock.deleteMany({
|
||||
where: { id: req.params.blockId, vehicleId: req.params.id },
|
||||
})
|
||||
res.json({ data: { success: true } })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user