import { and, asc, eq, gt, gte, inArray, isNull, lt, lte, ne } from 'drizzle-orm'; import { db } from '$lib/server/db'; import { bookings, contracts, contractRooms, rooms } from '$lib/server/db/schema'; const rentableContractRoomTypes = ['private_office', 'coworking_desk'] as const; export type BookingRoomConflict = { id: string; roomName: string; startsAt: string; endsAt: string; }; export type ContractRoomConflict = { contractId: string; roomId: string; roomName: string; startDate: string; endDate: string; }; export function bookingIntervalsOverlap( startsAt: string, endsAt: string, existingStartsAt: string, existingEndsAt: string ) { return startsAt < existingEndsAt && endsAt > existingStartsAt; } export function contractPeriodsOverlap( startDate: string, endDate: string, existingStartDate: string, existingEndDate: string ) { return startDate <= existingEndDate && endDate >= existingStartDate; } export async function findBookingRoomConflict({ organizationId, roomId, startsAt, endsAt, status, excludingBookingId }: { organizationId: string; roomId: string; startsAt: string; endsAt: string; status: string; excludingBookingId?: string; }): Promise { if (status === 'cancelled') return null; const baseConditions = [ eq(bookings.organizationId, organizationId), eq(bookings.roomId, roomId), isNull(bookings.archivedAt), ne(bookings.status, 'cancelled'), lt(bookings.startsAt, endsAt), gt(bookings.endsAt, startsAt) ]; const where = excludingBookingId ? and(...baseConditions, ne(bookings.id, excludingBookingId)) : and(...baseConditions); const [conflict] = await db .select({ id: bookings.id, roomName: rooms.name, startsAt: bookings.startsAt, endsAt: bookings.endsAt }) .from(bookings) .innerJoin(rooms, eq(bookings.roomId, rooms.id)) .where(where) .orderBy(asc(bookings.startsAt)) .limit(1); return conflict ?? null; } export async function findBookingContractRoomConflict({ organizationId, roomId, startsAt, endsAt, status }: { organizationId: string; roomId: string; startsAt: string; endsAt: string; status: string; }): Promise { if (status === 'cancelled') return null; const startDate = startsAt.slice(0, 10); const endDate = endsAt.slice(0, 10); const [conflict] = await db .select({ contractId: contracts.id, roomId: contractRooms.roomId, roomName: rooms.name, startDate: contracts.startDate, endDate: contracts.endDate }) .from(contractRooms) .innerJoin(contracts, eq(contractRooms.contractId, contracts.id)) .innerJoin(rooms, eq(contractRooms.roomId, rooms.id)) .where( and( eq(contracts.organizationId, organizationId), eq(contracts.status, 'active'), isNull(contracts.archivedAt), eq(contractRooms.roomId, roomId), eq(rooms.organizationId, organizationId), inArray(rooms.type, rentableContractRoomTypes), isNull(rooms.archivedAt), lte(contracts.startDate, endDate), gte(contracts.endDate, startDate) ) ) .orderBy(asc(contracts.startDate), asc(rooms.name)) .limit(1); return conflict ?? null; } export async function findActiveContractRoomConflict({ organizationId, roomIds, startDate, endDate, excludingContractId }: { organizationId: string; roomIds: string[]; startDate: string; endDate: string; excludingContractId?: string; }): Promise { const uniqueRoomIds = [...new Set(roomIds)]; if (uniqueRoomIds.length === 0) return null; const baseConditions = [ eq(contracts.organizationId, organizationId), eq(contracts.status, 'active'), isNull(contracts.archivedAt), inArray(contractRooms.roomId, uniqueRoomIds), eq(rooms.organizationId, organizationId), inArray(rooms.type, rentableContractRoomTypes), isNull(rooms.archivedAt), lte(contracts.startDate, endDate), gte(contracts.endDate, startDate) ]; const where = excludingContractId ? and(...baseConditions, ne(contracts.id, excludingContractId)) : and(...baseConditions); const [conflict] = await db .select({ contractId: contracts.id, roomId: contractRooms.roomId, roomName: rooms.name, startDate: contracts.startDate, endDate: contracts.endDate }) .from(contractRooms) .innerJoin(contracts, eq(contractRooms.contractId, contracts.id)) .innerJoin(rooms, eq(contractRooms.roomId, rooms.id)) .where(where) .orderBy(asc(contracts.startDate), asc(rooms.name)) .limit(1); return conflict ?? null; } export function bookingConflictMessage(conflict: BookingRoomConflict) { return `${conflict.roomName} is already booked from ${conflict.startsAt} to ${conflict.endsAt}.`; } export function contractConflictMessage(conflict: ContractRoomConflict) { return `${conflict.roomName} is already assigned to an active contract from ${conflict.startDate} to ${conflict.endDate}.`; }