Files
clearity/src/routes/dashboard/clients/[id]/bookings/+page.server.ts
T
kdaniel2410 cbd2fb2921 refactor: scope dashboard records by organization
Attach clients, rooms, services, bookings, contacts, addresses, invoices, and contracts to organizations.

Filter dashboard loads and related option lists by the active organization from the session.

Ensure create, update, and archive actions only affect records in the active organization.
2026-06-06 14:14:12 +01:00

168 lines
5.2 KiB
TypeScript

import { and, asc, eq, isNull } from 'drizzle-orm';
import { message, 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 { archiveSchema } from '$lib/schemas/shared.schema';
import { bookingCreateSchema, bookingEditSchema } from '$lib/schemas/bookings.schema';
import type { Actions, PageServerLoad } from './$types';
async function loadOptions(organizationId: string) {
const [clientRows, roomRows, serviceRows] = await Promise.all([
db
.select({ id: clients.id, name: clients.name })
.from(clients)
.where(and(eq(clients.organizationId, organizationId), isNull(clients.archivedAt)))
.orderBy(asc(clients.name)),
db
.select({ id: rooms.id, name: rooms.name })
.from(rooms)
.where(and(eq(rooms.organizationId, organizationId), isNull(rooms.archivedAt)))
.orderBy(asc(rooms.name)),
db
.select({ id: services.id, name: services.name })
.from(services)
.where(and(eq(services.organizationId, organizationId), isNull(services.archivedAt)))
.orderBy(asc(services.name))
]);
return {
clients: clientRows.map((client) => ({ label: client.name, value: client.id })),
rooms: roomRows.map((room) => ({ label: room.name, value: room.id })),
services: [
{ label: 'No service', value: '' },
...serviceRows.map((service) => ({ label: service.name, value: service.id }))
]
};
}
export const load: PageServerLoad = async ({ locals, params }) => {
const { activeOrganizationId } = await loadOrganizationContext(locals);
const records = await db
.select()
.from(bookings)
.where(
and(
eq(bookings.clientId, params.id),
eq(bookings.organizationId, activeOrganizationId),
isNull(bookings.archivedAt)
)
)
.orderBy(asc(bookings.startsAt));
return {
records: records.map((record) => ({
...record,
formValues: {
id: record.id,
clientId: record.clientId ?? '',
roomId: record.roomId ?? '',
serviceId: record.serviceId ?? '',
startsAt: record.startsAt ?? '',
endsAt: record.endsAt ?? '',
status: record.status ?? '',
notes: record.notes ?? ''
}
})),
options: await loadOptions(activeOrganizationId),
createForm: await superValidate({ clientId: params.id }, zod4(bookingCreateSchema), {
id: 'bookings-create'
}),
editForm: await superValidate(zod4(bookingEditSchema), { id: 'bookings-edit' }),
archiveForm: await superValidate(zod4(archiveSchema), { id: 'bookings-archive' })
};
};
export const actions: Actions = {
create: async ({ locals, params, request }) => {
const { activeOrganizationId } = await loadOrganizationContext(locals);
const formData = await request.formData();
formData.set('clientId', params.id);
const form = await superValidate(formData, zod4(bookingCreateSchema), {
id: 'bookings-create'
});
if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 });
try {
await db.insert(bookings).values({
id: crypto.randomUUID(),
organizationId: activeOrganizationId,
clientId: form.data.clientId,
roomId: form.data.roomId,
serviceId: form.data.serviceId || null,
startsAt: form.data.startsAt,
endsAt: form.data.endsAt,
status: form.data.status,
notes: form.data.notes || null,
updatedAt: new Date(),
createdAt: new Date()
});
} catch {
return message(form, 'Unable to create booking.', { status: 400 });
}
return message(form, 'Booking created.');
},
edit: async ({ locals, params, request }) => {
const { activeOrganizationId } = await loadOrganizationContext(locals);
const formData = await request.formData();
formData.set('clientId', params.id);
const form = await superValidate(formData, zod4(bookingEditSchema), {
id: 'bookings-edit'
});
if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 });
try {
await db
.update(bookings)
.set({
clientId: form.data.clientId,
roomId: form.data.roomId,
serviceId: form.data.serviceId || null,
startsAt: form.data.startsAt,
endsAt: form.data.endsAt,
status: form.data.status,
notes: form.data.notes || null,
updatedAt: new Date()
})
.where(
and(
eq(bookings.id, form.data.id),
eq(bookings.clientId, params.id),
eq(bookings.organizationId, activeOrganizationId)
)
);
} catch {
return message(form, 'Unable to update booking.', { status: 400 });
}
return message(form, 'Booking updated.');
},
archive: async ({ locals, params, request }) => {
const { activeOrganizationId } = await loadOrganizationContext(locals);
const form = await superValidate(await request.formData(), zod4(archiveSchema), {
id: 'bookings-archive'
});
if (!form.valid) return message(form, 'Booking id is required.', { status: 400 });
await db
.update(bookings)
.set({ archivedAt: new Date(), updatedAt: new Date() })
.where(
and(
eq(bookings.id, form.data.id),
eq(bookings.clientId, params.id),
eq(bookings.organizationId, activeOrganizationId)
)
);
return message(form, 'Booking archived.');
}
};