189 lines
4.8 KiB
TypeScript
189 lines
4.8 KiB
TypeScript
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<BookingRoomConflict | null> {
|
|
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<ContractRoomConflict | null> {
|
|
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<ContractRoomConflict | null> {
|
|
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}.`;
|
|
}
|