feat: prevent room scheduling conflicts #3
@@ -1,9 +1,10 @@
|
||||
import { and, asc, eq, isNull } from 'drizzle-orm';
|
||||
import { message, superValidate } from 'sveltekit-superforms/server';
|
||||
import { message, setError, superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import { db } from '$lib/server/db';
|
||||
import { bookings, clients, rooms, services } from '$lib/server/db/schema';
|
||||
import { loadOrganizationContext } from '$lib/server/organizations';
|
||||
import { bookingConflictMessage, findBookingRoomConflict } from '$lib/server/scheduling';
|
||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
||||
import { bookingCreateSchema, bookingEditSchema } from '$lib/schemas/bookings.schema';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
@@ -92,6 +93,15 @@ export const actions: Actions = {
|
||||
|
||||
if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 });
|
||||
|
||||
const conflict = await findBookingRoomConflict({
|
||||
organizationId: activeOrganizationId,
|
||||
roomId: form.data.roomId,
|
||||
startsAt: form.data.startsAt,
|
||||
endsAt: form.data.endsAt,
|
||||
status: form.data.status
|
||||
});
|
||||
if (conflict) return setError(form, 'roomId', bookingConflictMessage(conflict));
|
||||
|
||||
try {
|
||||
await db.insert(bookings).values({
|
||||
id: crypto.randomUUID(),
|
||||
@@ -123,6 +133,16 @@ export const actions: Actions = {
|
||||
|
||||
if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 });
|
||||
|
||||
const conflict = await findBookingRoomConflict({
|
||||
organizationId: activeOrganizationId,
|
||||
roomId: form.data.roomId,
|
||||
startsAt: form.data.startsAt,
|
||||
endsAt: form.data.endsAt,
|
||||
status: form.data.status,
|
||||
excludingBookingId: form.data.id
|
||||
});
|
||||
if (conflict) return setError(form, 'roomId', bookingConflictMessage(conflict));
|
||||
|
||||
try {
|
||||
await db
|
||||
.update(bookings)
|
||||
@@ -140,7 +160,8 @@ export const actions: Actions = {
|
||||
and(
|
||||
eq(bookings.id, form.data.id),
|
||||
eq(bookings.clientId, params.id),
|
||||
eq(bookings.organizationId, activeOrganizationId)
|
||||
eq(bookings.organizationId, activeOrganizationId),
|
||||
isNull(bookings.archivedAt)
|
||||
)
|
||||
);
|
||||
} catch {
|
||||
@@ -165,7 +186,8 @@ export const actions: Actions = {
|
||||
and(
|
||||
eq(bookings.id, form.data.id),
|
||||
eq(bookings.clientId, params.id),
|
||||
eq(bookings.organizationId, activeOrganizationId)
|
||||
eq(bookings.organizationId, activeOrganizationId),
|
||||
isNull(bookings.archivedAt)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import { db } from '$lib/server/db';
|
||||
import { contracts, contractRooms, rooms, services } from '$lib/server/db/schema';
|
||||
import { loadOrganizationContext } from '$lib/server/organizations';
|
||||
import { contractConflictMessage, findActiveContractRoomConflict } from '$lib/server/scheduling';
|
||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
||||
import {
|
||||
contractCreateSchema,
|
||||
@@ -263,6 +264,34 @@ export const actions: Actions = {
|
||||
return message(form, invalidSelection.message, { status: 400 });
|
||||
}
|
||||
|
||||
const [existingContract] = await db
|
||||
.select({ status: contracts.status })
|
||||
.from(contracts)
|
||||
.where(
|
||||
and(
|
||||
eq(contracts.id, form.data.id),
|
||||
eq(contracts.clientId, params.id),
|
||||
eq(contracts.organizationId, activeOrganizationId),
|
||||
isNull(contracts.archivedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existingContract) return message(form, 'Choose a valid contract.', { status: 400 });
|
||||
|
||||
if (existingContract.status === 'active') {
|
||||
const conflict = await findActiveContractRoomConflict({
|
||||
organizationId: activeOrganizationId,
|
||||
roomIds: form.data.roomIds,
|
||||
startDate: form.data.startDate,
|
||||
endDate: form.data.endDate,
|
||||
excludingContractId: form.data.id
|
||||
});
|
||||
if (conflict) {
|
||||
return message(form, contractConflictMessage(conflict), { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
@@ -280,7 +309,8 @@ export const actions: Actions = {
|
||||
and(
|
||||
eq(contracts.id, form.data.id),
|
||||
eq(contracts.clientId, params.id),
|
||||
eq(contracts.organizationId, activeOrganizationId)
|
||||
eq(contracts.organizationId, activeOrganizationId),
|
||||
isNull(contracts.archivedAt)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -311,7 +341,11 @@ export const actions: Actions = {
|
||||
if (!form.valid) return message(form, 'Choose a valid contract action.', { status: 400 });
|
||||
|
||||
const [contract] = await db
|
||||
.select({ status: contracts.status })
|
||||
.select({
|
||||
status: contracts.status,
|
||||
startDate: contracts.startDate,
|
||||
endDate: contracts.endDate
|
||||
})
|
||||
.from(contracts)
|
||||
.where(
|
||||
and(
|
||||
@@ -332,6 +366,23 @@ export const actions: Actions = {
|
||||
return message(form, 'That contract status change is not allowed.', { status: 400 });
|
||||
}
|
||||
|
||||
if (form.data.targetStatus === 'active') {
|
||||
const roomLinks = await db
|
||||
.select({ roomId: contractRooms.roomId })
|
||||
.from(contractRooms)
|
||||
.where(eq(contractRooms.contractId, form.data.id));
|
||||
const conflict = await findActiveContractRoomConflict({
|
||||
organizationId: activeOrganizationId,
|
||||
roomIds: roomLinks.map((link) => link.roomId),
|
||||
startDate: contract.startDate,
|
||||
endDate: contract.endDate,
|
||||
excludingContractId: form.data.id
|
||||
});
|
||||
if (conflict) {
|
||||
return message(form, contractConflictMessage(conflict), { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
await db
|
||||
.update(contracts)
|
||||
.set({ status: form.data.targetStatus, updatedAt: new Date() })
|
||||
@@ -339,7 +390,8 @@ export const actions: Actions = {
|
||||
and(
|
||||
eq(contracts.id, form.data.id),
|
||||
eq(contracts.clientId, params.id),
|
||||
eq(contracts.organizationId, activeOrganizationId)
|
||||
eq(contracts.organizationId, activeOrganizationId),
|
||||
isNull(contracts.archivedAt)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -361,7 +413,8 @@ export const actions: Actions = {
|
||||
and(
|
||||
eq(contracts.id, form.data.id),
|
||||
eq(contracts.clientId, params.id),
|
||||
eq(contracts.organizationId, activeOrganizationId)
|
||||
eq(contracts.organizationId, activeOrganizationId),
|
||||
isNull(contracts.archivedAt)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user