update: split workspace pages into reusable components
This commit is contained in:
@@ -2,7 +2,7 @@ import { and, asc, eq, inArray, isNull } from 'drizzle-orm';
|
||||
import { message, setError, superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import { db } from '$lib/server/db';
|
||||
import { contracts, rooms, services } from '$lib/server/db/schema';
|
||||
import { contracts, contractRooms, rooms, services } from '$lib/server/db/schema';
|
||||
import { loadOrganizationContext } from '$lib/server/organizations';
|
||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
||||
import {
|
||||
@@ -39,7 +39,6 @@ async function loadOptions(organizationId: string) {
|
||||
|
||||
return {
|
||||
rooms: [
|
||||
{ label: 'No room', value: '' },
|
||||
...roomRows.map((room) => ({
|
||||
label: `${room.name} · ${room.type === 'private_office' ? 'Private office' : 'Coworking desk'}`,
|
||||
value: room.id,
|
||||
@@ -63,11 +62,9 @@ export const load: PageServerLoad = async ({ locals, params }) => {
|
||||
const records = await db
|
||||
.select({
|
||||
contract: contracts,
|
||||
roomName: rooms.name,
|
||||
serviceName: services.name
|
||||
})
|
||||
.from(contracts)
|
||||
.leftJoin(rooms, eq(contracts.roomId, rooms.id))
|
||||
.leftJoin(services, eq(contracts.serviceId, services.id))
|
||||
.where(
|
||||
and(
|
||||
@@ -77,29 +74,57 @@ export const load: PageServerLoad = async ({ locals, params }) => {
|
||||
)
|
||||
)
|
||||
.orderBy(asc(contracts.startDate));
|
||||
const contractIds = records.map((record) => record.contract.id);
|
||||
const roomLinks =
|
||||
contractIds.length > 0
|
||||
? await db
|
||||
.select({
|
||||
contractId: contractRooms.contractId,
|
||||
roomId: rooms.id,
|
||||
roomName: rooms.name
|
||||
})
|
||||
.from(contractRooms)
|
||||
.innerJoin(rooms, eq(contractRooms.roomId, rooms.id))
|
||||
.where(inArray(contractRooms.contractId, contractIds))
|
||||
.orderBy(asc(rooms.name))
|
||||
: [];
|
||||
const roomsByContract = new Map<string, { id: string; name: string }[]>();
|
||||
for (const link of roomLinks) {
|
||||
const existing = roomsByContract.get(link.contractId) ?? [];
|
||||
existing.push({ id: link.roomId, name: link.roomName });
|
||||
roomsByContract.set(link.contractId, existing);
|
||||
}
|
||||
|
||||
return {
|
||||
records: records.map((record) => ({
|
||||
...record.contract,
|
||||
roomName: record.roomName,
|
||||
serviceName: record.serviceName,
|
||||
formValues: {
|
||||
id: record.contract.id,
|
||||
clientId: record.contract.clientId ?? '',
|
||||
roomId: record.contract.roomId ?? '',
|
||||
serviceId: record.contract.serviceId ?? '',
|
||||
licenseFeeGbp: record.contract.licenseFeeGbp,
|
||||
depositGbp: record.contract.depositGbp,
|
||||
startDate: record.contract.startDate ?? '',
|
||||
endDate: record.contract.endDate ?? '',
|
||||
notes: record.contract.notes ?? ''
|
||||
}
|
||||
})),
|
||||
options: await loadOptions(activeOrganizationId),
|
||||
createForm: await superValidate({ clientId: params.id }, zod4(contractCreateSchema), {
|
||||
id: 'contracts-create',
|
||||
errors: false
|
||||
records: records.map((record) => {
|
||||
const linkedRooms = roomsByContract.get(record.contract.id) ?? [];
|
||||
return {
|
||||
...record.contract,
|
||||
roomName: linkedRooms.map((room) => room.name).join(', '),
|
||||
roomNames: linkedRooms.map((room) => room.name),
|
||||
roomIds: linkedRooms.map((room) => room.id),
|
||||
serviceName: record.serviceName,
|
||||
formValues: {
|
||||
id: record.contract.id,
|
||||
clientId: record.contract.clientId ?? '',
|
||||
roomIds: linkedRooms.map((room) => room.id),
|
||||
serviceId: record.contract.serviceId ?? '',
|
||||
licenseFeeGbp: record.contract.licenseFeeGbp,
|
||||
depositGbp: record.contract.depositGbp,
|
||||
startDate: record.contract.startDate ?? '',
|
||||
endDate: record.contract.endDate ?? ''
|
||||
}
|
||||
};
|
||||
}),
|
||||
options: await loadOptions(activeOrganizationId),
|
||||
createForm: await superValidate(
|
||||
{ clientId: params.id, roomIds: [] },
|
||||
zod4(contractCreateSchema),
|
||||
{
|
||||
id: 'contracts-create',
|
||||
errors: false
|
||||
}
|
||||
),
|
||||
editForm: await superValidate(zod4(contractEditSchema), {
|
||||
id: 'contracts-edit',
|
||||
errors: false
|
||||
@@ -117,26 +142,26 @@ export const load: PageServerLoad = async ({ locals, params }) => {
|
||||
|
||||
async function selectionError(
|
||||
organizationId: string,
|
||||
roomId: string,
|
||||
roomIds: string[],
|
||||
serviceId: string
|
||||
): Promise<{ field: 'roomId' | 'serviceId'; message: string } | null> {
|
||||
if (roomId) {
|
||||
const [room] = await db
|
||||
): Promise<{ field: 'roomIds' | 'serviceId'; message: string } | null> {
|
||||
if (roomIds.length > 0) {
|
||||
const uniqueRoomIds = [...new Set(roomIds)];
|
||||
const validRooms = await db
|
||||
.select({ id: rooms.id })
|
||||
.from(rooms)
|
||||
.where(
|
||||
and(
|
||||
eq(rooms.id, roomId),
|
||||
inArray(rooms.id, uniqueRoomIds),
|
||||
eq(rooms.organizationId, organizationId),
|
||||
inArray(rooms.type, ['private_office', 'coworking_desk']),
|
||||
isNull(rooms.archivedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
);
|
||||
|
||||
if (!room) {
|
||||
if (validRooms.length !== uniqueRoomIds.length) {
|
||||
return {
|
||||
field: 'roomId',
|
||||
field: 'roomIds',
|
||||
message: 'Select a valid private office or coworking desk.'
|
||||
};
|
||||
}
|
||||
@@ -173,26 +198,42 @@ export const actions: Actions = {
|
||||
if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 });
|
||||
const invalidSelection = await selectionError(
|
||||
activeOrganizationId,
|
||||
form.data.roomId,
|
||||
form.data.roomIds,
|
||||
form.data.serviceId
|
||||
);
|
||||
if (invalidSelection) return setError(form, invalidSelection.field, invalidSelection.message);
|
||||
if (invalidSelection) {
|
||||
if (invalidSelection.field === 'serviceId') {
|
||||
return setError(form, invalidSelection.field, invalidSelection.message);
|
||||
}
|
||||
return message(form, invalidSelection.message, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await db.insert(contracts).values({
|
||||
id: crypto.randomUUID(),
|
||||
organizationId: activeOrganizationId,
|
||||
clientId: form.data.clientId,
|
||||
roomId: form.data.roomId || null,
|
||||
serviceId: form.data.serviceId || null,
|
||||
licenseFeeGbp: form.data.licenseFeeGbp,
|
||||
depositGbp: form.data.depositGbp,
|
||||
startDate: form.data.startDate,
|
||||
endDate: form.data.endDate,
|
||||
status: 'draft',
|
||||
notes: form.data.notes || null,
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date()
|
||||
const contractId = crypto.randomUUID();
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(contracts).values({
|
||||
id: contractId,
|
||||
organizationId: activeOrganizationId,
|
||||
clientId: form.data.clientId,
|
||||
serviceId: form.data.serviceId || null,
|
||||
licenseFeeGbp: form.data.licenseFeeGbp,
|
||||
depositGbp: form.data.depositGbp,
|
||||
startDate: form.data.startDate,
|
||||
endDate: form.data.endDate,
|
||||
status: 'draft',
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
const uniqueRoomIds = [...new Set(form.data.roomIds)];
|
||||
if (uniqueRoomIds.length > 0) {
|
||||
await tx.insert(contractRooms).values(
|
||||
uniqueRoomIds.map((roomId) => ({
|
||||
contractId,
|
||||
roomId
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
return message(form, 'Unable to create contract.', { status: 400 });
|
||||
@@ -212,32 +253,48 @@ export const actions: Actions = {
|
||||
if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 });
|
||||
const invalidSelection = await selectionError(
|
||||
activeOrganizationId,
|
||||
form.data.roomId,
|
||||
form.data.roomIds,
|
||||
form.data.serviceId
|
||||
);
|
||||
if (invalidSelection) return setError(form, invalidSelection.field, invalidSelection.message);
|
||||
if (invalidSelection) {
|
||||
if (invalidSelection.field === 'serviceId') {
|
||||
return setError(form, invalidSelection.field, invalidSelection.message);
|
||||
}
|
||||
return message(form, invalidSelection.message, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await db
|
||||
.update(contracts)
|
||||
.set({
|
||||
clientId: form.data.clientId,
|
||||
roomId: form.data.roomId || null,
|
||||
serviceId: form.data.serviceId || null,
|
||||
licenseFeeGbp: form.data.licenseFeeGbp,
|
||||
depositGbp: form.data.depositGbp,
|
||||
startDate: form.data.startDate,
|
||||
endDate: form.data.endDate,
|
||||
notes: form.data.notes || null,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(contracts.id, form.data.id),
|
||||
eq(contracts.clientId, params.id),
|
||||
eq(contracts.organizationId, activeOrganizationId)
|
||||
)
|
||||
);
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(contracts)
|
||||
.set({
|
||||
clientId: form.data.clientId,
|
||||
serviceId: form.data.serviceId || null,
|
||||
licenseFeeGbp: form.data.licenseFeeGbp,
|
||||
depositGbp: form.data.depositGbp,
|
||||
startDate: form.data.startDate,
|
||||
endDate: form.data.endDate,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(contracts.id, form.data.id),
|
||||
eq(contracts.clientId, params.id),
|
||||
eq(contracts.organizationId, activeOrganizationId)
|
||||
)
|
||||
);
|
||||
|
||||
await tx.delete(contractRooms).where(eq(contractRooms.contractId, form.data.id));
|
||||
const uniqueRoomIds = [...new Set(form.data.roomIds)];
|
||||
if (uniqueRoomIds.length > 0) {
|
||||
await tx.insert(contractRooms).values(
|
||||
uniqueRoomIds.map((roomId) => ({
|
||||
contractId: form.data.id,
|
||||
roomId
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
return message(form, 'Unable to update contract.', { status: 400 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user