feat: add client workspace management

This commit is contained in:
2026-06-05 23:25:36 +01:00
parent 930e83344c
commit 631a5112f4
72 changed files with 6423 additions and 0 deletions
@@ -0,0 +1,130 @@
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 { invoices, clients } from '$lib/server/db/schema';
import { archiveSchema } from '$lib/schemas/shared.schema';
import { invoiceCreateSchema, invoiceEditSchema } from '$lib/schemas/invoices.schema';
import type { Actions, PageServerLoad } from './$types';
async function loadOptions() {
const clientRows = await db
.select({ id: clients.id, name: clients.name })
.from(clients)
.where(isNull(clients.archivedAt))
.orderBy(asc(clients.name));
return {
clients: clientRows.map((client) => ({ label: client.name, value: client.id }))
};
}
export const load: PageServerLoad = async ({ params }) => {
const records = await db
.select()
.from(invoices)
.where(and(eq(invoices.clientId, params.id), isNull(invoices.archivedAt)))
.orderBy(asc(invoices.invoiceNumber));
return {
records: records.map((record) => ({
...record,
formValues: {
id: record.id,
clientId: record.clientId ?? '',
invoiceNumber: record.invoiceNumber ?? '',
issueDate: record.issueDate ?? '',
dueDate: record.dueDate ?? '',
status: record.status ?? '',
subtotalGbp: record.subtotalGbp ?? '',
taxGbp: record.taxGbp ?? '',
totalGbp: record.totalGbp ?? '',
notes: record.notes ?? ''
}
})),
options: await loadOptions(),
createForm: await superValidate({ clientId: params.id }, zod4(invoiceCreateSchema), {
id: 'invoices-create'
}),
editForm: await superValidate(zod4(invoiceEditSchema), { id: 'invoices-edit' }),
archiveForm: await superValidate(zod4(archiveSchema), { id: 'invoices-archive' })
};
};
export const actions: Actions = {
create: async ({ params, request }) => {
const formData = await request.formData();
formData.set('clientId', params.id);
const form = await superValidate(formData, zod4(invoiceCreateSchema), {
id: 'invoices-create'
});
if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 });
try {
await db.insert(invoices).values({
id: crypto.randomUUID(),
clientId: form.data.clientId,
invoiceNumber: form.data.invoiceNumber,
issueDate: form.data.issueDate,
dueDate: form.data.dueDate,
status: form.data.status,
subtotalGbp: form.data.subtotalGbp,
taxGbp: form.data.taxGbp,
totalGbp: form.data.totalGbp,
notes: form.data.notes || null,
updatedAt: new Date(),
createdAt: new Date()
});
} catch {
return message(form, 'Unable to create invoice.', { status: 400 });
}
return message(form, 'Invoice created.');
},
edit: async ({ params, request }) => {
const formData = await request.formData();
formData.set('clientId', params.id);
const form = await superValidate(formData, zod4(invoiceEditSchema), { id: 'invoices-edit' });
if (!form.valid) return message(form, 'Check the highlighted fields.', { status: 400 });
try {
await db
.update(invoices)
.set({
clientId: form.data.clientId,
invoiceNumber: form.data.invoiceNumber,
issueDate: form.data.issueDate,
dueDate: form.data.dueDate,
status: form.data.status,
subtotalGbp: form.data.subtotalGbp,
taxGbp: form.data.taxGbp,
totalGbp: form.data.totalGbp,
notes: form.data.notes || null,
updatedAt: new Date()
})
.where(and(eq(invoices.id, form.data.id), eq(invoices.clientId, params.id)));
} catch {
return message(form, 'Unable to update invoice.', { status: 400 });
}
return message(form, 'Invoice updated.');
},
archive: async ({ params, request }) => {
const form = await superValidate(await request.formData(), zod4(archiveSchema), {
id: 'invoices-archive'
});
if (!form.valid) return message(form, 'Invoice id is required.', { status: 400 });
await db
.update(invoices)
.set({ archivedAt: new Date(), updatedAt: new Date() })
.where(and(eq(invoices.id, form.data.id), eq(invoices.clientId, params.id)));
return message(form, 'Invoice archived.');
}
};