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