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 @@