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
+16
View File
@@ -0,0 +1,16 @@
import { z } from 'zod';
import { booleanString, idSchema, optionalText, requiredText } from './shared.schema';
export const addressCreateSchema = z.object({
clientId: requiredText('Client'),
label: requiredText('Label', 80),
line1: requiredText('Address line 1'),
line2: optionalText(160),
city: requiredText('City', 100),
region: optionalText(100),
postcode: requiredText('Postcode', 24),
country: requiredText('Country', 80).default('United Kingdom'),
isPrimary: booleanString
});
export const addressEditSchema = addressCreateSchema.extend(idSchema.shape);
+14
View File
@@ -0,0 +1,14 @@
import { z } from 'zod';
import { idSchema, optionalText, requiredText } from './shared.schema';
export const bookingCreateSchema = z.object({
clientId: requiredText('Client'),
roomId: requiredText('Room'),
serviceId: optionalText(80),
startsAt: requiredText('Start time', 40),
endsAt: requiredText('End time', 40),
status: z.enum(['booked', 'confirmed', 'completed', 'cancelled']).default('booked'),
notes: optionalText(1000)
});
export const bookingEditSchema = bookingCreateSchema.extend(idSchema.shape);
+27
View File
@@ -0,0 +1,27 @@
import { z } from 'zod';
import { idSchema, optionalEmail, optionalText, optionalUrl, requiredText } from './shared.schema';
export const clientCreateSchema = z.object({
name: requiredText('Client name'),
type: z.enum(['business', 'sport', 'individual']).default('business'),
status: z.enum(['active', 'prospect', 'paused']).default('active'),
website: optionalUrl,
notes: optionalText(1000)
});
export const clientEditSchema = clientCreateSchema.extend(idSchema.shape);
export const clientOnboardingCreateSchema = clientCreateSchema.extend({
primaryContactName: requiredText('Primary contact name'),
primaryContactRole: optionalText(120),
primaryContactEmail: optionalEmail,
primaryContactPhone: optionalText(80),
primaryContactNotes: optionalText(1000),
primaryAddressLabel: requiredText('Address label', 80).default('Primary'),
primaryAddressLine1: requiredText('Address line 1'),
primaryAddressLine2: optionalText(160),
primaryAddressCity: requiredText('City', 100),
primaryAddressRegion: optionalText(100),
primaryAddressPostcode: requiredText('Postcode', 24),
primaryAddressCountry: requiredText('Country', 80).default('United Kingdom')
});
+20
View File
@@ -0,0 +1,20 @@
import { z } from 'zod';
import {
booleanString,
idSchema,
optionalEmail,
optionalText,
requiredText
} from './shared.schema';
export const contactCreateSchema = z.object({
clientId: requiredText('Client'),
name: requiredText('Contact name'),
role: optionalText(120),
email: optionalEmail,
phone: optionalText(80),
isPrimary: booleanString,
notes: optionalText(1000)
});
export const contactEditSchema = contactCreateSchema.extend(idSchema.shape);
+14
View File
@@ -0,0 +1,14 @@
import { z } from 'zod';
import { idSchema, moneyGbp, optionalText, requiredText } from './shared.schema';
export const contractCreateSchema = z.object({
clientId: requiredText('Client'),
title: requiredText('Contract title'),
startDate: requiredText('Start date', 40),
endDate: optionalText(40),
status: z.enum(['draft', 'active', 'expired', 'cancelled']).default('draft'),
valueGbp: moneyGbp,
notes: optionalText(1000)
});
export const contractEditSchema = contractCreateSchema.extend(idSchema.shape);
+16
View File
@@ -0,0 +1,16 @@
import { z } from 'zod';
import { idSchema, moneyGbp, optionalText, requiredText } from './shared.schema';
export const invoiceCreateSchema = z.object({
clientId: requiredText('Client'),
invoiceNumber: requiredText('Invoice number', 80),
issueDate: requiredText('Issue date', 40),
dueDate: requiredText('Due date', 40),
status: z.enum(['draft', 'sent', 'paid', 'overdue', 'void']).default('draft'),
subtotalGbp: moneyGbp,
taxGbp: moneyGbp,
totalGbp: moneyGbp,
notes: optionalText(1000)
});
export const invoiceEditSchema = invoiceCreateSchema.extend(idSchema.shape);
+39
View File
@@ -0,0 +1,39 @@
import { z } from 'zod';
export const idSchema = z.object({
id: z.string().uuid()
});
export const archiveSchema = idSchema;
export const requiredText = (label: string, max = 160) =>
z.string().trim().min(1, `${label} is required.`).max(max, `${label} is too long.`);
export const optionalText = (max = 500) => z.string().trim().max(max).optional().default('');
export const optionalEmail = z
.string()
.trim()
.email('Enter a valid email address.')
.or(z.literal(''))
.default('');
export const optionalUrl = z
.string()
.trim()
.url('Enter a valid URL.')
.or(z.literal(''))
.default('');
export const moneyGbp = z.coerce
.number({ error: 'Enter an amount in GBP.' })
.min(0, 'Amount cannot be negative.')
.default(0);
export const positiveInt = (label: string) =>
z.coerce
.number({ error: `${label} is required.` })
.int()
.min(1, `${label} must be at least 1.`);
export const booleanString = z.enum(['true', 'false']).default('false');