diff --git a/package.json b/package.json
index 3ac8b96..8c49099 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.1",
"prettier-plugin-tailwindcss": "^0.7.2",
+ "runed": "^0.37.1",
"shadcn-svelte": "^1.2.7",
"svelte": "^5.55.2",
"svelte-check": "^4.4.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 23a2364..f9ff036 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -98,6 +98,9 @@ importers:
prettier-plugin-tailwindcss:
specifier: ^0.7.2
version: 0.7.4(prettier-plugin-svelte@3.5.2(prettier@3.8.3)(svelte@5.55.9(@typescript-eslint/types@8.59.4)))(prettier@3.8.3)
+ runed:
+ specifier: ^0.37.1
+ version: 0.37.1(@sveltejs/kit@2.61.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.9(@typescript-eslint/types@8.59.4))(vite@8.0.14(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.3)))(svelte@5.55.9(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.14(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.3)))(svelte@5.55.9(@typescript-eslint/types@8.59.4))(zod@4.4.3)
shadcn-svelte:
specifier: ^1.2.7
version: 1.2.7(svelte@5.55.9(@typescript-eslint/types@8.59.4))
diff --git a/src/lib/components/list-search.svelte b/src/lib/components/list-search.svelte
new file mode 100644
index 0000000..9761980
--- /dev/null
+++ b/src/lib/components/list-search.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+ {#if value}
+
{resultCount} of {totalCount} shown
+ {/if}
+
diff --git a/src/lib/list-search.svelte.ts b/src/lib/list-search.svelte.ts
new file mode 100644
index 0000000..991f0a7
--- /dev/null
+++ b/src/lib/list-search.svelte.ts
@@ -0,0 +1,24 @@
+import { useSearchParams } from 'runed/kit';
+import { z } from 'zod';
+import { filterRecords } from '$lib/record-utils';
+
+export const listSearchParamsSchema = z.object({
+ q: z.string().default('')
+});
+
+export function createListSearch(records: () => readonly T[]) {
+ const params = useSearchParams(listSearchParamsSchema, {
+ debounce: 150,
+ noScroll: true,
+ pushHistory: false
+ });
+
+ const filtered = $derived(filterRecords(records(), params.q));
+
+ return {
+ params,
+ get filtered() {
+ return filtered;
+ }
+ };
+}
diff --git a/src/routes/dashboard/addresses/+page.svelte b/src/routes/dashboard/addresses/+page.svelte
index 4f0e203..3e464ef 100644
--- a/src/routes/dashboard/addresses/+page.svelte
+++ b/src/routes/dashboard/addresses/+page.svelte
@@ -1,14 +1,25 @@
@@ -85,7 +60,14 @@
-
+
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import CreateAdminDialog from './create-admin-dialog.svelte';
import DeleteAdminDialog from './delete-admin-dialog.svelte';
import AdminsTable from './admins-table.svelte';
@@ -7,9 +9,14 @@
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { createAdminSchema, deleteAdminSchema, editAdminSchema } from '$lib/schemas/admin';
+ import { formatDate } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.users);
+ const listData = $derived({ ...data, users: listSearch.filtered });
+
let createOpen = $state(false);
let editingUserId = $state(null);
let deletingUserId = $state(null);
@@ -41,38 +48,6 @@
const { form: editData, enhance: enhanceEdit } = editForm;
const { form: deleteData, enhance: enhanceDelete } = deleteForm;
- function formatDate(value: Date) {
- return new Intl.DateTimeFormat('en-GB', {
- day: '2-digit',
- month: 'short',
- year: 'numeric'
- }).format(value);
- }
-
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') {
- return value[0];
- }
- }
-
- return 'Check the highlighted fields.';
- }
-
function openEdit(user: PageData['users'][number]) {
editForm.reset({
data: {
@@ -110,7 +85,14 @@
-
+
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import BookingsTable from './bookings-table.svelte';
import ArchiveBookingDialog from './archive-booking-dialog.svelte';
import { toast } from 'svelte-sonner';
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import {
+ formatValue,
+ formatDate,
+ recordName as getRecordName,
+ relationLabel as getRelationLabel
+ } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let archivingId = $state(null);
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
@@ -23,48 +35,8 @@
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
function relationLabel(value: unknown, optionsKey: 'clients' | 'rooms' | 'services') {
- if (typeof value !== 'string') return 'Not set';
- return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
- }
-
- function formatDate(value: unknown, withTime = false) {
- if (!value || typeof value !== 'string') return 'Not set';
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return value;
- return new Intl.DateTimeFormat('en-GB', {
- day: '2-digit',
- month: 'short',
- year: 'numeric',
- ...(withTime ? { hour: '2-digit', minute: '2-digit' } : {})
- }).format(date);
+ return getRelationLabel(data.options, value, optionsKey);
}
function openArchive(record: RecordRow) {
@@ -73,15 +45,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(
- item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this booking'
- );
+ return getRecordName(record, 'this booking');
}
@@ -99,7 +63,21 @@
-
+
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import CreateClientDialog from './create-client-dialog.svelte';
import ClientsTable from './clients-table.svelte';
import EditClientDialog from './edit-client-dialog.svelte';
@@ -7,11 +9,16 @@
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import { formatValue, recordName as getRecordName } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import { clientEditSchema, clientOnboardingCreateSchema } from '$lib/schemas/clients.schema';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let createOpen = $state(false);
let createStep = $state(0);
let editingId = $state(null);
@@ -49,33 +56,6 @@
const { form: editData, enhance: enhanceEdit } = editForm;
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
function openEdit(record: RecordRow) {
editForm.reset({ data: record.formValues as never });
editingId = record.id;
@@ -87,13 +67,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this client');
+ return getRecordName(record, 'this client');
}
@@ -119,7 +93,14 @@
/>
-
+
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import CreateAddressDialog from './create-address-dialog.svelte';
import AddressesTable from './addresses-table.svelte';
import EditAddressDialog from './edit-address-dialog.svelte';
@@ -7,11 +9,20 @@
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import {
+ formatValue,
+ recordName as getRecordName,
+ relationLabel as getRelationLabel
+ } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import { addressCreateSchema, addressEditSchema } from '$lib/schemas/addresses.schema';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let createOpen = $state(false);
let editingId = $state(null);
let archivingId = $state(null);
@@ -44,36 +55,8 @@
const { form: editData, enhance: enhanceEdit } = editForm;
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
function relationLabel(value: unknown, optionsKey: 'clients') {
- if (typeof value !== 'string') return 'Not set';
- return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
+ return getRelationLabel(data.options, value, optionsKey);
}
function openEdit(record: RecordRow) {
@@ -87,15 +70,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(
- item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this address'
- );
+ return getRecordName(record, 'this address');
}
@@ -113,7 +88,21 @@
-
+
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import CreateBookingDialog from './create-booking-dialog.svelte';
import BookingsTable from './bookings-table.svelte';
import EditBookingDialog from './edit-booking-dialog.svelte';
@@ -7,11 +9,21 @@
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import {
+ formatValue,
+ formatDate,
+ recordName as getRecordName,
+ relationLabel as getRelationLabel
+ } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import { bookingCreateSchema, bookingEditSchema } from '$lib/schemas/bookings.schema';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let createOpen = $state(false);
let editingId = $state(null);
let archivingId = $state(null);
@@ -44,48 +56,8 @@
const { form: editData, enhance: enhanceEdit } = editForm;
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
function relationLabel(value: unknown, optionsKey: 'clients' | 'rooms' | 'services') {
- if (typeof value !== 'string') return 'Not set';
- return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
- }
-
- function formatDate(value: unknown, withTime = false) {
- if (!value || typeof value !== 'string') return 'Not set';
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return value;
- return new Intl.DateTimeFormat('en-GB', {
- day: '2-digit',
- month: 'short',
- year: 'numeric',
- ...(withTime ? { hour: '2-digit', minute: '2-digit' } : {})
- }).format(date);
+ return getRelationLabel(data.options, value, optionsKey);
}
function openEdit(record: RecordRow) {
@@ -99,15 +71,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(
- item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this booking'
- );
+ return getRecordName(record, 'this booking');
}
@@ -127,8 +91,15 @@
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import CreateContactDialog from './create-contact-dialog.svelte';
import ContactsTable from './contacts-table.svelte';
import EditContactDialog from './edit-contact-dialog.svelte';
@@ -7,11 +9,20 @@
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import {
+ formatValue,
+ recordName as getRecordName,
+ relationLabel as getRelationLabel
+ } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import { contactCreateSchema, contactEditSchema } from '$lib/schemas/contacts.schema';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let createOpen = $state(false);
let editingId = $state(null);
let archivingId = $state(null);
@@ -44,36 +55,8 @@
const { form: editData, enhance: enhanceEdit } = editForm;
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
function relationLabel(value: unknown, optionsKey: 'clients') {
- if (typeof value !== 'string') return 'Not set';
- return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
+ return getRelationLabel(data.options, value, optionsKey);
}
function openEdit(record: RecordRow) {
@@ -87,15 +70,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(
- item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this contact'
- );
+ return getRecordName(record, 'this contact');
}
@@ -113,7 +88,21 @@
-
+
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import CreateContractDialog from './create-contract-dialog.svelte';
import ContractsTable from './contracts-table.svelte';
import EditContractDialog from './edit-contract-dialog.svelte';
@@ -7,11 +9,22 @@
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import {
+ formatValue,
+ formatMoney,
+ formatDate,
+ recordName as getRecordName,
+ relationLabel as getRelationLabel
+ } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import { contractCreateSchema, contractEditSchema } from '$lib/schemas/contracts.schema';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let createOpen = $state(false);
let editingId = $state(null);
let archivingId = $state(null);
@@ -44,53 +57,8 @@
const { form: editData, enhance: enhanceEdit } = editForm;
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
function relationLabel(value: unknown, optionsKey: 'clients') {
- if (typeof value !== 'string') return 'Not set';
- return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
- }
-
- function formatMoney(value: unknown) {
- const amount = typeof value === 'number' ? value : Number(value ?? 0);
- return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(amount);
- }
-
- function formatDate(value: unknown, withTime = false) {
- if (!value || typeof value !== 'string') return 'Not set';
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return value;
- return new Intl.DateTimeFormat('en-GB', {
- day: '2-digit',
- month: 'short',
- year: 'numeric',
- ...(withTime ? { hour: '2-digit', minute: '2-digit' } : {})
- }).format(date);
+ return getRelationLabel(data.options, value, optionsKey);
}
function openEdit(record: RecordRow) {
@@ -104,15 +72,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(
- item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this contract'
- );
+ return getRecordName(record, 'this contract');
}
@@ -130,8 +90,15 @@
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import CreateInvoiceDialog from './create-invoice-dialog.svelte';
import InvoicesTable from './invoices-table.svelte';
import EditInvoiceDialog from './edit-invoice-dialog.svelte';
@@ -7,11 +9,22 @@
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import {
+ formatValue,
+ formatMoney,
+ formatDate,
+ recordName as getRecordName,
+ relationLabel as getRelationLabel
+ } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import { invoiceCreateSchema, invoiceEditSchema } from '$lib/schemas/invoices.schema';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let createOpen = $state(false);
let editingId = $state(null);
let archivingId = $state(null);
@@ -44,53 +57,8 @@
const { form: editData, enhance: enhanceEdit } = editForm;
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
function relationLabel(value: unknown, optionsKey: 'clients') {
- if (typeof value !== 'string') return 'Not set';
- return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
- }
-
- function formatMoney(value: unknown) {
- const amount = typeof value === 'number' ? value : Number(value ?? 0);
- return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(amount);
- }
-
- function formatDate(value: unknown, withTime = false) {
- if (!value || typeof value !== 'string') return 'Not set';
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return value;
- return new Intl.DateTimeFormat('en-GB', {
- day: '2-digit',
- month: 'short',
- year: 'numeric',
- ...(withTime ? { hour: '2-digit', minute: '2-digit' } : {})
- }).format(date);
+ return getRelationLabel(data.options, value, optionsKey);
}
function openEdit(record: RecordRow) {
@@ -104,15 +72,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(
- item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this invoice'
- );
+ return getRecordName(record, 'this invoice');
}
@@ -130,8 +90,15 @@
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import ContactsTable from './contacts-table.svelte';
import ArchiveContactDialog from './archive-contact-dialog.svelte';
import { toast } from 'svelte-sonner';
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import {
+ formatValue,
+ recordName as getRecordName,
+ relationLabel as getRelationLabel
+ } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let archivingId = $state(null);
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
@@ -23,36 +34,8 @@
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
function relationLabel(value: unknown, optionsKey: 'clients') {
- if (typeof value !== 'string') return 'Not set';
- return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
+ return getRelationLabel(data.options, value, optionsKey);
}
function openArchive(record: RecordRow) {
@@ -61,15 +44,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(
- item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this contact'
- );
+ return getRecordName(record, 'this contact');
}
@@ -85,7 +60,14 @@
-
+
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import ContractsTable from './contracts-table.svelte';
import ArchiveContractDialog from './archive-contract-dialog.svelte';
import { toast } from 'svelte-sonner';
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import {
+ formatValue,
+ formatMoney,
+ formatDate,
+ recordName as getRecordName,
+ relationLabel as getRelationLabel
+ } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let archivingId = $state(null);
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
@@ -23,53 +36,8 @@
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
function relationLabel(value: unknown, optionsKey: 'clients') {
- if (typeof value !== 'string') return 'Not set';
- return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
- }
-
- function formatMoney(value: unknown) {
- const amount = typeof value === 'number' ? value : Number(value ?? 0);
- return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(amount);
- }
-
- function formatDate(value: unknown, withTime = false) {
- if (!value || typeof value !== 'string') return 'Not set';
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return value;
- return new Intl.DateTimeFormat('en-GB', {
- day: '2-digit',
- month: 'short',
- year: 'numeric',
- ...(withTime ? { hour: '2-digit', minute: '2-digit' } : {})
- }).format(date);
+ return getRelationLabel(data.options, value, optionsKey);
}
function openArchive(record: RecordRow) {
@@ -78,15 +46,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(
- item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this contract'
- );
+ return getRecordName(record, 'this contract');
}
@@ -102,8 +62,15 @@
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import InvoicesTable from './invoices-table.svelte';
import ArchiveInvoiceDialog from './archive-invoice-dialog.svelte';
import { toast } from 'svelte-sonner';
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import {
+ formatValue,
+ formatMoney,
+ formatDate,
+ recordName as getRecordName,
+ relationLabel as getRelationLabel
+ } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let archivingId = $state(null);
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
@@ -23,53 +36,8 @@
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
function relationLabel(value: unknown, optionsKey: 'clients') {
- if (typeof value !== 'string') return 'Not set';
- return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
- }
-
- function formatMoney(value: unknown) {
- const amount = typeof value === 'number' ? value : Number(value ?? 0);
- return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(amount);
- }
-
- function formatDate(value: unknown, withTime = false) {
- if (!value || typeof value !== 'string') return 'Not set';
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return value;
- return new Intl.DateTimeFormat('en-GB', {
- day: '2-digit',
- month: 'short',
- year: 'numeric',
- ...(withTime ? { hour: '2-digit', minute: '2-digit' } : {})
- }).format(date);
+ return getRelationLabel(data.options, value, optionsKey);
}
function openArchive(record: RecordRow) {
@@ -78,15 +46,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(
- item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this invoice'
- );
+ return getRecordName(record, 'this invoice');
}
@@ -102,8 +62,15 @@
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import CreateRoomDialog from './create-room-dialog.svelte';
import RoomsTable from './rooms-table.svelte';
import EditRoomDialog from './edit-room-dialog.svelte';
@@ -7,11 +9,16 @@
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import { formatValue, formatMoney, recordName as getRecordName } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import { roomCreateSchema, roomEditSchema } from '$lib/schemas/rooms.schema';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let createOpen = $state(false);
let editingId = $state(null);
let archivingId = $state(null);
@@ -44,39 +51,6 @@
const { form: editData, enhance: enhanceEdit } = editForm;
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
- function formatMoney(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- const amount = typeof value === 'number' ? value : Number(value ?? 0);
- return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(amount);
- }
-
function formatRoomType(value: unknown) {
const labels: Record = {
meeting_room: 'Meeting room',
@@ -98,13 +72,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this room');
+ return getRecordName(record, 'this room');
}
@@ -124,8 +92,15 @@
+
+
+ import { createListSearch } from '$lib/list-search.svelte';
+ import ListSearch from '$lib/components/list-search.svelte';
import CreateServiceDialog from './create-service-dialog.svelte';
import ServicesTable from './services-table.svelte';
import EditServiceDialog from './edit-service-dialog.svelte';
@@ -7,11 +9,16 @@
import { superForm } from 'sveltekit-superforms';
import { zod4Client } from 'sveltekit-superforms/adapters';
import { archiveSchema } from '$lib/schemas/shared.schema';
+ import { formatValue, formatMoney, recordName as getRecordName } from '$lib/record-utils';
+ import { handleFormToast } from '$lib/form-feedback';
import { serviceCreateSchema, serviceEditSchema } from '$lib/schemas/services.schema';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
+ const listSearch = createListSearch(() => data.records);
+ const listData = $derived({ ...data, records: listSearch.filtered });
+
let createOpen = $state(false);
let editingId = $state(null);
let archivingId = $state(null);
@@ -44,38 +51,6 @@
const { form: editData, enhance: enhanceEdit } = editForm;
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
- function handleFormToast(
- form: { valid: boolean; message?: string; errors: Record },
- id: string,
- onSuccess: () => void
- ) {
- if (form.valid) {
- if (form.message) toast.success(form.message, { id });
- onSuccess();
- return;
- }
-
- toast.error(form.message ?? firstError(form.errors), { id });
- }
-
- function firstError(errors: Record) {
- for (const value of Object.values(errors)) {
- if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
- }
-
- return 'Check the highlighted fields.';
- }
-
- function formatValue(value: unknown) {
- if (value === null || value === undefined || value === '') return 'Not set';
- return String(value);
- }
-
- function formatMoney(value: unknown) {
- const amount = typeof value === 'number' ? value : Number(value ?? 0);
- return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(amount);
- }
-
function openEdit(record: RecordRow) {
editForm.reset({ data: record.formValues as never });
editingId = record.id;
@@ -87,15 +62,7 @@
}
function recordName(record: Partial | undefined) {
- const item = record as Partial & {
- name?: unknown;
- title?: unknown;
- invoiceNumber?: unknown;
- label?: unknown;
- };
- return String(
- item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this service'
- );
+ return getRecordName(record, 'this service');
}
@@ -113,7 +80,21 @@
-
+
+
+