add car reservation feature
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import { prisma } from '../lib/prisma'
|
||||
|
||||
const BLOCKING_RESERVATION_STATUSES = ['DRAFT', 'CONFIRMED', 'ACTIVE'] as const
|
||||
|
||||
type AvailabilityStatus = 'AVAILABLE' | 'RESERVED' | 'MAINTENANCE' | 'UNAVAILABLE'
|
||||
|
||||
type DateRange = {
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
}
|
||||
|
||||
type AvailabilitySource = {
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
kind: 'RESERVATION' | 'MAINTENANCE' | 'BLOCK'
|
||||
}
|
||||
|
||||
export interface VehicleAvailabilitySummary {
|
||||
available: boolean
|
||||
status: AvailabilityStatus
|
||||
nextAvailableAt: Date | null
|
||||
blockingReason: 'RESERVATION' | 'MAINTENANCE' | 'BLOCK' | 'STATUS' | null
|
||||
}
|
||||
|
||||
function startOfTodayUtc() {
|
||||
const now = new Date()
|
||||
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
|
||||
}
|
||||
|
||||
function overlaps(range: DateRange, source: AvailabilitySource) {
|
||||
return source.startDate < range.endDate && source.endDate > range.startDate
|
||||
}
|
||||
|
||||
function getNextAvailableAt(anchor: Date, sources: AvailabilitySource[]) {
|
||||
const sorted = sources
|
||||
.filter((source) => source.endDate > anchor)
|
||||
.sort((a, b) => {
|
||||
const startDelta = a.startDate.getTime() - b.startDate.getTime()
|
||||
if (startDelta !== 0) return startDelta
|
||||
return a.endDate.getTime() - b.endDate.getTime()
|
||||
})
|
||||
|
||||
let candidate = anchor
|
||||
let advanced = false
|
||||
|
||||
for (const source of sorted) {
|
||||
if (source.endDate <= candidate) continue
|
||||
if (source.startDate > candidate) break
|
||||
candidate = new Date(Math.max(candidate.getTime(), source.endDate.getTime()))
|
||||
advanced = true
|
||||
}
|
||||
|
||||
return advanced ? candidate : null
|
||||
}
|
||||
|
||||
export async function getVehicleAvailabilitySummary(
|
||||
vehicleId: string,
|
||||
{
|
||||
range,
|
||||
includeDraftReservations = true,
|
||||
}: {
|
||||
range?: DateRange
|
||||
includeDraftReservations?: boolean
|
||||
} = {},
|
||||
): Promise<VehicleAvailabilitySummary> {
|
||||
const vehicle = await prisma.vehicle.findUniqueOrThrow({
|
||||
where: { id: vehicleId },
|
||||
select: { status: true },
|
||||
})
|
||||
|
||||
if (vehicle.status === 'OUT_OF_SERVICE') {
|
||||
return {
|
||||
available: false,
|
||||
status: 'UNAVAILABLE',
|
||||
nextAvailableAt: null,
|
||||
blockingReason: 'STATUS',
|
||||
}
|
||||
}
|
||||
|
||||
const anchor = range?.startDate ?? startOfTodayUtc()
|
||||
const reservationStatuses = includeDraftReservations
|
||||
? [...BLOCKING_RESERVATION_STATUSES]
|
||||
: BLOCKING_RESERVATION_STATUSES.filter((status) => status !== 'DRAFT')
|
||||
|
||||
const [reservations, blocks] = await Promise.all([
|
||||
prisma.reservation.findMany({
|
||||
where: {
|
||||
vehicleId,
|
||||
status: { in: reservationStatuses },
|
||||
endDate: { gt: anchor },
|
||||
},
|
||||
select: { startDate: true, endDate: true },
|
||||
orderBy: { startDate: 'asc' },
|
||||
}),
|
||||
prisma.vehicleCalendarBlock.findMany({
|
||||
where: {
|
||||
vehicleId,
|
||||
endDate: { gt: anchor },
|
||||
},
|
||||
select: { startDate: true, endDate: true, type: true },
|
||||
orderBy: { startDate: 'asc' },
|
||||
}),
|
||||
])
|
||||
|
||||
const sources: AvailabilitySource[] = [
|
||||
...reservations.map((reservation) => ({
|
||||
startDate: reservation.startDate,
|
||||
endDate: reservation.endDate,
|
||||
kind: 'RESERVATION' as const,
|
||||
})),
|
||||
...blocks.map((block) => ({
|
||||
startDate: block.startDate,
|
||||
endDate: block.endDate,
|
||||
kind: block.type === 'MAINTENANCE' ? 'MAINTENANCE' as const : 'BLOCK' as const,
|
||||
})),
|
||||
]
|
||||
|
||||
const overlappingSources = range ? sources.filter((source) => overlaps(range, source)) : sources.filter((source) => source.startDate <= anchor && source.endDate > anchor)
|
||||
const currentSource = overlappingSources.sort((a, b) => {
|
||||
const priority = { MAINTENANCE: 0, BLOCK: 1, RESERVATION: 2 }
|
||||
return priority[a.kind] - priority[b.kind]
|
||||
})[0]
|
||||
|
||||
if (vehicle.status === 'MAINTENANCE') {
|
||||
return {
|
||||
available: false,
|
||||
status: 'MAINTENANCE',
|
||||
nextAvailableAt: getNextAvailableAt(anchor, sources) ?? null,
|
||||
blockingReason: currentSource?.kind ?? 'STATUS',
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentSource) {
|
||||
return {
|
||||
available: true,
|
||||
status: 'AVAILABLE',
|
||||
nextAvailableAt: null,
|
||||
blockingReason: null,
|
||||
}
|
||||
}
|
||||
|
||||
const nextAvailableAnchor = currentSource.startDate > anchor ? currentSource.startDate : anchor
|
||||
|
||||
return {
|
||||
available: false,
|
||||
status: currentSource.kind === 'RESERVATION' ? 'RESERVED' : currentSource.kind === 'MAINTENANCE' ? 'MAINTENANCE' : 'UNAVAILABLE',
|
||||
nextAvailableAt: getNextAvailableAt(nextAvailableAnchor, sources),
|
||||
blockingReason: currentSource.kind,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user