From 1ea8ce21358314a89509bb08fdfbc299ea700e4d Mon Sep 17 00:00:00 2001 From: Daniel Kirby Date: Wed, 24 Jun 2026 13:40:01 +0100 Subject: [PATCH] refactor: centralize form and record helpers --- src/lib/components/first-admin-form.svelte | 111 +++--- src/lib/components/login-form.svelte | 97 ++--- src/lib/components/money-field.svelte | 49 ++- src/lib/form-feedback.ts | 42 ++ src/lib/record-utils.ts | 79 ++++ .../admins/create-admin-dialog.svelte | 99 +++-- .../dashboard/admins/edit-admin-dialog.svelte | 101 +++-- .../dashboard/clients/[id]/+layout.svelte | 156 +++----- .../addresses/create-address-dialog.svelte | 171 ++++---- .../[id]/addresses/edit-address-dialog.svelte | 171 ++++---- .../bookings/create-booking-dialog.svelte | 153 ++++--- .../[id]/bookings/edit-booking-dialog.svelte | 153 ++++--- .../contacts/create-contact-dialog.svelte | 131 +++--- .../[id]/contacts/edit-contact-dialog.svelte | 131 +++--- .../contracts/create-contract-dialog.svelte | 115 +++--- .../contracts/edit-contract-dialog.svelte | 115 +++--- .../invoices/create-invoice-dialog.svelte | 117 +++--- .../[id]/invoices/edit-invoice-dialog.svelte | 117 +++--- .../clients/create-client-dialog.svelte | 373 ++++++++---------- .../clients/edit-client-dialog.svelte | 133 +++---- .../dashboard/rooms/create-room-dialog.svelte | 23 +- .../dashboard/rooms/edit-room-dialog.svelte | 23 +- .../dashboard/rooms/room-type-fields.svelte | 121 +++--- .../services/create-service-dialog.svelte | 83 ++-- .../services/edit-service-dialog.svelte | 83 ++-- 25 files changed, 1390 insertions(+), 1557 deletions(-) create mode 100644 src/lib/form-feedback.ts create mode 100644 src/lib/record-utils.ts diff --git a/src/lib/components/first-admin-form.svelte b/src/lib/components/first-admin-form.svelte index 29f85f6..74a6200 100644 --- a/src/lib/components/first-admin-form.svelte +++ b/src/lib/components/first-admin-form.svelte @@ -1,7 +1,7 @@
@@ -71,58 +62,50 @@ Set up the initial Better Auth admin account for this workspace.
- - - - {#snippet children({ props })} - Name - - {/snippet} - - - - - - - - {#snippet children({ props })} - Email - - {/snippet} - - - - - - - - {#snippet children({ props })} - Password - - {/snippet} - - - - - - - + + + {#snippet children({ props })} + Name + + {/snippet} + + + + + + {#snippet children({ props })} + Email + + {/snippet} + + + + + + {#snippet children({ props })} + Password + + {/snippet} + + + + diff --git a/src/lib/components/login-form.svelte b/src/lib/components/login-form.svelte index 9106410..b2ab9d2 100644 --- a/src/lib/components/login-form.svelte +++ b/src/lib/components/login-form.svelte @@ -1,14 +1,15 @@ -
+
-
+
Clearity @@ -69,45 +62,39 @@ Use the administrator account created for this workspace.
- - - - {#snippet children({ props })} - Email - - {/snippet} - - - - - - - - {#snippet children({ props })} - Password - - {/snippet} - - - + + + {#snippet children({ props })} + Email + + {/snippet} + + + + + + {#snippet children({ props })} + Password + + {/snippet} + + New users are created manually by an administrator; public registration is disabled. - - - - + + diff --git a/src/lib/components/money-field.svelte b/src/lib/components/money-field.svelte index 7474e8f..9f9db77 100644 --- a/src/lib/components/money-field.svelte +++ b/src/lib/components/money-field.svelte @@ -1,7 +1,6 @@ - - - - {#snippet children({ props })} - {label} - - - £ - - - - {/snippet} - - - - + + + {#snippet children({ props })} + {label} + + + £ + + + + {/snippet} + + + diff --git a/src/lib/form-feedback.ts b/src/lib/form-feedback.ts new file mode 100644 index 0000000..bf854ba --- /dev/null +++ b/src/lib/form-feedback.ts @@ -0,0 +1,42 @@ +import { toast } from 'svelte-sonner'; + +type FormFeedback = { + valid: boolean; + message?: string; + errors: Record; +}; + +export function handleFormToast(form: FormFeedback, id: string, onSuccess: () => void) { + if (form.valid) { + if (form.message) toast.success(form.message, { id }); + onSuccess(); + return; + } + + toast.error(form.message ?? firstFormError(form.errors), { id }); +} + +export function firstFormError(errors: Record) { + return findFirstError(errors) ?? 'Check the highlighted fields.'; +} + +function findFirstError(value: unknown): string | undefined { + if (typeof value === 'string') return value; + + if (Array.isArray(value)) { + for (const item of value) { + const error = findFirstError(item); + if (error) return error; + } + return undefined; + } + + if (value && typeof value === 'object') { + for (const item of Object.values(value)) { + const error = findFirstError(item); + if (error) return error; + } + } + + return undefined; +} diff --git a/src/lib/record-utils.ts b/src/lib/record-utils.ts new file mode 100644 index 0000000..f1b5513 --- /dev/null +++ b/src/lib/record-utils.ts @@ -0,0 +1,79 @@ +type NamedRecord = Partial>; + +type RelationOption = { + label: string; + value: string; +}; + +const gbpFormatter = new Intl.NumberFormat('en-GB', { + style: 'currency', + currency: 'GBP' +}); + +const dateFormatter = new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric' +}); + +const dateTimeFormatter = new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' +}); + +export function formatValue(value: unknown) { + if (value === null || value === undefined || value === '') return 'Not set'; + return String(value); +} + +export function formatMoney(value: unknown, fallback = 'Not set') { + if (value === null || value === undefined || value === '') return fallback; + + const amount = typeof value === 'number' ? value : Number(value); + return gbpFormatter.format(Number.isFinite(amount) ? amount : 0); +} + +export function formatDate(value: unknown, withTime = false) { + if (!value) return 'Not set'; + + const date = value instanceof Date ? value : new Date(String(value)); + if (Number.isNaN(date.getTime())) return 'Not set'; + + return (withTime ? dateTimeFormatter : dateFormatter).format(date); +} + +export function recordName(record: object | undefined, fallback: string) { + const item = record as NamedRecord | undefined; + + return String(item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? fallback); +} + +export function relationLabel( + options: Options, + value: unknown, + optionsKey: Key +) { + if (typeof value !== 'string') return 'Not set'; + + const optionList = options[optionsKey] as RelationOption[] | undefined; + return optionList?.find((option) => option.value === value)?.label ?? value; +} + +export function filterRecords(records: readonly T[], query: string) { + const normalizedQuery = query.trim().toLowerCase(); + if (!normalizedQuery) return [...records]; + + return records.filter((record) => searchableText(record).includes(normalizedQuery)); +} + +function searchableText(value: unknown): string { + if (value === null || value === undefined) return ''; + if (value instanceof Date) return value.toISOString().toLowerCase(); + if (typeof value !== 'object') return String(value).toLowerCase(); + if (Array.isArray(value)) return value.map(searchableText).join(' '); + + return Object.values(value).map(searchableText).join(' '); +} diff --git a/src/routes/dashboard/admins/create-admin-dialog.svelte b/src/routes/dashboard/admins/create-admin-dialog.svelte index 9cd76ee..f8253c6 100644 --- a/src/routes/dashboard/admins/create-admin-dialog.svelte +++ b/src/routes/dashboard/admins/create-admin-dialog.svelte @@ -1,8 +1,7 @@ {#snippet numberField(name: string, label: string, min = 0)} - - - - {#snippet children({ props })} - {label} - - {/snippet} - - - - + + + {#snippet children({ props })} + {label} + + {/snippet} + + + {/snippet} - - - - {#snippet children({ props })} - - {/snippet} - - Type - - - {#each roomTypes as roomType (roomType.value)} - - {roomType.label} - - {/each} - + + + {#snippet children({ props })} + + {/snippet} + + Type + + + {#each roomTypes as roomType (roomType.value)} + + {roomType.label} + + {/each} + - - {@render numberField('sqFt', 'Sq ft.', 1)} -
- {#each bookablePriceFields as field (field.name)} - - {/each} -
- {@render numberField('maxAttendees', 'Max attendees', 1)} -
- - - {@render numberField('sqFt', 'Sq ft.', 1)} - {@render numberField('workstations', 'Number of workstations', 1)} - - - - + + {@render numberField('sqFt', 'Sq ft.', 1)} +
{#each bookablePriceFields as field (field.name)} {/each} - - - - - +
+ {@render numberField('maxAttendees', 'Max attendees', 1)} +
+ + + {@render numberField('sqFt', 'Sq ft.', 1)} + {@render numberField('workstations', 'Number of workstations', 1)} + + + + + {#each bookablePriceFields as field (field.name)} + + {/each} + +
+ +
diff --git a/src/routes/dashboard/services/create-service-dialog.svelte b/src/routes/dashboard/services/create-service-dialog.svelte index 1b724d7..0b7f4fa 100644 --- a/src/routes/dashboard/services/create-service-dialog.svelte +++ b/src/routes/dashboard/services/create-service-dialog.svelte @@ -1,9 +1,8 @@