Compare commits

..

1 Commits

Author SHA1 Message Date
kdaniel2410 013b81879c fix: block bookings for contracted rooms 2026-06-25 14:33:11 +01:00
3 changed files with 79 additions and 47 deletions
+48
View File
@@ -82,6 +82,54 @@ export async function findBookingRoomConflict({
return conflict ?? null; 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({ export async function findActiveContractRoomConflict({
organizationId, organizationId,
roomIds, roomIds,
@@ -4,13 +4,16 @@ import { zod4 } from 'sveltekit-superforms/adapters';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { bookings, clients, rooms, services } from '$lib/server/db/schema'; import { bookings, clients, rooms, services } from '$lib/server/db/schema';
import { loadOrganizationContext } from '$lib/server/organizations'; import { loadOrganizationContext } from '$lib/server/organizations';
import { bookingConflictMessage, findBookingRoomConflict } from '$lib/server/scheduling'; import {
bookingConflictMessage,
contractConflictMessage,
findBookingContractRoomConflict,
findBookingRoomConflict
} from '$lib/server/scheduling';
import { archiveSchema } from '$lib/schemas/shared.schema'; import { archiveSchema } from '$lib/schemas/shared.schema';
import { bookingCreateSchema, bookingEditSchema } from '$lib/schemas/bookings.schema'; import { bookingCreateSchema, bookingEditSchema } from '$lib/schemas/bookings.schema';
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
const bookingRoomType = 'meeting_room';
async function loadOptions(organizationId: string) { async function loadOptions(organizationId: string) {
const [clientRows, roomRows, serviceRows] = await Promise.all([ const [clientRows, roomRows, serviceRows] = await Promise.all([
db db
@@ -21,13 +24,7 @@ async function loadOptions(organizationId: string) {
db db
.select({ id: rooms.id, name: rooms.name }) .select({ id: rooms.id, name: rooms.name })
.from(rooms) .from(rooms)
.where( .where(and(eq(rooms.organizationId, organizationId), isNull(rooms.archivedAt)))
and(
eq(rooms.organizationId, organizationId),
eq(rooms.type, bookingRoomType),
isNull(rooms.archivedAt)
)
)
.orderBy(asc(rooms.name)), .orderBy(asc(rooms.name)),
db db
.select({ id: services.id, name: services.name }) .select({ id: services.id, name: services.name })
@@ -46,23 +43,6 @@ async function loadOptions(organizationId: string) {
}; };
} }
async function isValidBookingRoom(organizationId: string, roomId: string) {
const [room] = await db
.select({ id: rooms.id })
.from(rooms)
.where(
and(
eq(rooms.id, roomId),
eq(rooms.organizationId, organizationId),
eq(rooms.type, bookingRoomType),
isNull(rooms.archivedAt)
)
)
.limit(1);
return Boolean(room);
}
export const load: PageServerLoad = async ({ locals, params }) => { export const load: PageServerLoad = async ({ locals, params }) => {
const { activeOrganizationId } = await loadOrganizationContext(locals); const { activeOrganizationId } = await loadOrganizationContext(locals);
const records = await db const records = await db
@@ -118,10 +98,6 @@ export const actions: Actions = {
if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 }); if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 });
if (!(await isValidBookingRoom(activeOrganizationId, form.data.roomId))) {
return setError(form, 'roomId', 'Select a valid meeting room.');
}
const conflict = await findBookingRoomConflict({ const conflict = await findBookingRoomConflict({
organizationId: activeOrganizationId, organizationId: activeOrganizationId,
roomId: form.data.roomId, roomId: form.data.roomId,
@@ -131,6 +107,17 @@ export const actions: Actions = {
}); });
if (conflict) return setError(form, 'roomId', bookingConflictMessage(conflict)); if (conflict) return setError(form, 'roomId', bookingConflictMessage(conflict));
const contractConflict = await findBookingContractRoomConflict({
organizationId: activeOrganizationId,
roomId: form.data.roomId,
startsAt: form.data.startsAt,
endsAt: form.data.endsAt,
status: form.data.status
});
if (contractConflict) {
return setError(form, 'roomId', contractConflictMessage(contractConflict));
}
try { try {
await db.insert(bookings).values({ await db.insert(bookings).values({
id: crypto.randomUUID(), id: crypto.randomUUID(),
@@ -162,10 +149,6 @@ export const actions: Actions = {
if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 }); if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 });
if (!(await isValidBookingRoom(activeOrganizationId, form.data.roomId))) {
return setError(form, 'roomId', 'Select a valid meeting room.');
}
const conflict = await findBookingRoomConflict({ const conflict = await findBookingRoomConflict({
organizationId: activeOrganizationId, organizationId: activeOrganizationId,
roomId: form.data.roomId, roomId: form.data.roomId,
@@ -176,6 +159,17 @@ export const actions: Actions = {
}); });
if (conflict) return setError(form, 'roomId', bookingConflictMessage(conflict)); if (conflict) return setError(form, 'roomId', bookingConflictMessage(conflict));
const contractConflict = await findBookingContractRoomConflict({
organizationId: activeOrganizationId,
roomId: form.data.roomId,
startsAt: form.data.startsAt,
endsAt: form.data.endsAt,
status: form.data.status
});
if (contractConflict) {
return setError(form, 'roomId', contractConflictMessage(contractConflict));
}
try { try {
await db await db
.update(bookings) .update(bookings)
@@ -13,8 +13,6 @@ import {
} from '$lib/schemas/contracts.schema'; } from '$lib/schemas/contracts.schema';
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
const contractRoomTypes = ['private_office', 'coworking_desk'] as const;
async function loadOptions(organizationId: string) { async function loadOptions(organizationId: string) {
const [roomRows, serviceRows] = await Promise.all([ const [roomRows, serviceRows] = await Promise.all([
db db
@@ -28,7 +26,7 @@ async function loadOptions(organizationId: string) {
.where( .where(
and( and(
eq(rooms.organizationId, organizationId), eq(rooms.organizationId, organizationId),
inArray(rooms.type, contractRoomTypes), inArray(rooms.type, ['private_office', 'coworking_desk']),
isNull(rooms.archivedAt) isNull(rooms.archivedAt)
) )
) )
@@ -157,7 +155,7 @@ async function selectionError(
and( and(
inArray(rooms.id, uniqueRoomIds), inArray(rooms.id, uniqueRoomIds),
eq(rooms.organizationId, organizationId), eq(rooms.organizationId, organizationId),
inArray(rooms.type, contractRoomTypes), inArray(rooms.type, ['private_office', 'coworking_desk']),
isNull(rooms.archivedAt) isNull(rooms.archivedAt)
) )
); );
@@ -373,14 +371,6 @@ export const actions: Actions = {
.select({ roomId: contractRooms.roomId }) .select({ roomId: contractRooms.roomId })
.from(contractRooms) .from(contractRooms)
.where(eq(contractRooms.contractId, form.data.id)); .where(eq(contractRooms.contractId, form.data.id));
const invalidSelection = await selectionError(
activeOrganizationId,
roomLinks.map((link) => link.roomId),
''
);
if (invalidSelection) {
return message(form, invalidSelection.message, { status: 400 });
}
const conflict = await findActiveContractRoomConflict({ const conflict = await findActiveContractRoomConflict({
organizationId: activeOrganizationId, organizationId: activeOrganizationId,
roomIds: roomLinks.map((link) => link.roomId), roomIds: roomLinks.map((link) => link.roomId),