feat: add runed-backed list search
This commit is contained in:
@@ -48,6 +48,7 @@
|
|||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"prettier-plugin-svelte": "^3.5.1",
|
"prettier-plugin-svelte": "^3.5.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
|
"runed": "^0.37.1",
|
||||||
"shadcn-svelte": "^1.2.7",
|
"shadcn-svelte": "^1.2.7",
|
||||||
"svelte": "^5.55.2",
|
"svelte": "^5.55.2",
|
||||||
"svelte-check": "^4.4.6",
|
"svelte-check": "^4.4.6",
|
||||||
|
|||||||
Generated
+3
@@ -98,6 +98,9 @@ importers:
|
|||||||
prettier-plugin-tailwindcss:
|
prettier-plugin-tailwindcss:
|
||||||
specifier: ^0.7.2
|
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)
|
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:
|
shadcn-svelte:
|
||||||
specifier: ^1.2.7
|
specifier: ^1.2.7
|
||||||
version: 1.2.7(svelte@5.55.9(@typescript-eslint/types@8.59.4))
|
version: 1.2.7(svelte@5.55.9(@typescript-eslint/types@8.59.4))
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SearchIcon from '@lucide/svelte/icons/search';
|
||||||
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
value = $bindable(''),
|
||||||
|
placeholder = 'Search records...',
|
||||||
|
totalCount,
|
||||||
|
resultCount
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
placeholder?: string;
|
||||||
|
totalCount: number;
|
||||||
|
resultCount: number;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div class="relative w-full sm:max-w-xs">
|
||||||
|
<SearchIcon
|
||||||
|
class="pointer-events-none absolute start-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<Input type="search" class="ps-8" {placeholder} bind:value />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if value}
|
||||||
|
<p class="text-sm text-muted-foreground">{resultCount} of {totalCount} shown</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -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<T>(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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,14 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import AddressesTable from './addresses-table.svelte';
|
import AddressesTable from './addresses-table.svelte';
|
||||||
import ArchiveAddressDialog from './archive-address-dialog.svelte';
|
import ArchiveAddressDialog from './archive-address-dialog.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
|
|
||||||
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
|
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
|
||||||
@@ -23,36 +34,8 @@
|
|||||||
|
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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') {
|
function relationLabel(value: unknown, optionsKey: 'clients') {
|
||||||
if (typeof value !== 'string') return 'Not set';
|
return getRelationLabel(data.options, value, optionsKey);
|
||||||
return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openArchive(record: RecordRow) {
|
function openArchive(record: RecordRow) {
|
||||||
@@ -61,15 +44,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this address');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(
|
|
||||||
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this address'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -85,7 +60,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddressesTable {data} {openArchive} {formatValue} {relationLabel} {recordName} />
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search addresses..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AddressesTable data={listData} {openArchive} {formatValue} {relationLabel} {recordName} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ArchiveAddressDialog
|
<ArchiveAddressDialog
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import CreateAdminDialog from './create-admin-dialog.svelte';
|
import CreateAdminDialog from './create-admin-dialog.svelte';
|
||||||
import DeleteAdminDialog from './delete-admin-dialog.svelte';
|
import DeleteAdminDialog from './delete-admin-dialog.svelte';
|
||||||
import AdminsTable from './admins-table.svelte';
|
import AdminsTable from './admins-table.svelte';
|
||||||
@@ -7,9 +9,14 @@
|
|||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { createAdminSchema, deleteAdminSchema, editAdminSchema } from '$lib/schemas/admin';
|
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';
|
import type { PageData } from './$types';
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.users);
|
||||||
|
const listData = $derived({ ...data, users: listSearch.filtered });
|
||||||
|
|
||||||
let createOpen = $state(false);
|
let createOpen = $state(false);
|
||||||
let editingUserId = $state<string | null>(null);
|
let editingUserId = $state<string | null>(null);
|
||||||
let deletingUserId = $state<string | null>(null);
|
let deletingUserId = $state<string | null>(null);
|
||||||
@@ -41,38 +48,6 @@
|
|||||||
const { form: editData, enhance: enhanceEdit } = editForm;
|
const { form: editData, enhance: enhanceEdit } = editForm;
|
||||||
const { form: deleteData, enhance: enhanceDelete } = deleteForm;
|
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<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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]) {
|
function openEdit(user: PageData['users'][number]) {
|
||||||
editForm.reset({
|
editForm.reset({
|
||||||
data: {
|
data: {
|
||||||
@@ -110,7 +85,14 @@
|
|||||||
<CreateAdminDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
<CreateAdminDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AdminsTable {data} {openEdit} {openDelete} {formatDate} />
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search admins..."
|
||||||
|
totalCount={data.users.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AdminsTable data={listData} {openEdit} {openDelete} {formatDate} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditAdminDialog
|
<EditAdminDialog
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import BookingsTable from './bookings-table.svelte';
|
import BookingsTable from './bookings-table.svelte';
|
||||||
import ArchiveBookingDialog from './archive-booking-dialog.svelte';
|
import ArchiveBookingDialog from './archive-booking-dialog.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
|
|
||||||
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
|
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
|
||||||
@@ -23,48 +35,8 @@
|
|||||||
|
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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') {
|
function relationLabel(value: unknown, optionsKey: 'clients' | 'rooms' | 'services') {
|
||||||
if (typeof value !== 'string') return 'Not set';
|
return getRelationLabel(data.options, value, optionsKey);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openArchive(record: RecordRow) {
|
function openArchive(record: RecordRow) {
|
||||||
@@ -73,15 +45,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this booking');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(
|
|
||||||
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this booking'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -99,7 +63,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BookingsTable {data} {openArchive} {formatValue} {formatDate} {relationLabel} {recordName} />
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search bookings..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BookingsTable
|
||||||
|
data={listData}
|
||||||
|
{openArchive}
|
||||||
|
{formatValue}
|
||||||
|
{formatDate}
|
||||||
|
{relationLabel}
|
||||||
|
{recordName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ArchiveBookingDialog
|
<ArchiveBookingDialog
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import CreateClientDialog from './create-client-dialog.svelte';
|
import CreateClientDialog from './create-client-dialog.svelte';
|
||||||
import ClientsTable from './clients-table.svelte';
|
import ClientsTable from './clients-table.svelte';
|
||||||
import EditClientDialog from './edit-client-dialog.svelte';
|
import EditClientDialog from './edit-client-dialog.svelte';
|
||||||
@@ -7,11 +9,16 @@
|
|||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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 { clientEditSchema, clientOnboardingCreateSchema } from '$lib/schemas/clients.schema';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let createOpen = $state(false);
|
let createOpen = $state(false);
|
||||||
let createStep = $state(0);
|
let createStep = $state(0);
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
@@ -49,33 +56,6 @@
|
|||||||
const { form: editData, enhance: enhanceEdit } = editForm;
|
const { form: editData, enhance: enhanceEdit } = editForm;
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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) {
|
function openEdit(record: RecordRow) {
|
||||||
editForm.reset({ data: record.formValues as never });
|
editForm.reset({ data: record.formValues as never });
|
||||||
editingId = record.id;
|
editingId = record.id;
|
||||||
@@ -87,13 +67,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this client');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this client');
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -119,7 +93,14 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ClientsTable {data} {openEdit} {openArchive} {formatValue} {recordName} />
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search clients..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ClientsTable data={listData} {openEdit} {openArchive} {formatValue} {recordName} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditClientDialog
|
<EditClientDialog
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import CreateAddressDialog from './create-address-dialog.svelte';
|
import CreateAddressDialog from './create-address-dialog.svelte';
|
||||||
import AddressesTable from './addresses-table.svelte';
|
import AddressesTable from './addresses-table.svelte';
|
||||||
import EditAddressDialog from './edit-address-dialog.svelte';
|
import EditAddressDialog from './edit-address-dialog.svelte';
|
||||||
@@ -7,11 +9,20 @@
|
|||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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 { addressCreateSchema, addressEditSchema } from '$lib/schemas/addresses.schema';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let createOpen = $state(false);
|
let createOpen = $state(false);
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
@@ -44,36 +55,8 @@
|
|||||||
const { form: editData, enhance: enhanceEdit } = editForm;
|
const { form: editData, enhance: enhanceEdit } = editForm;
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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') {
|
function relationLabel(value: unknown, optionsKey: 'clients') {
|
||||||
if (typeof value !== 'string') return 'Not set';
|
return getRelationLabel(data.options, value, optionsKey);
|
||||||
return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(record: RecordRow) {
|
function openEdit(record: RecordRow) {
|
||||||
@@ -87,15 +70,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this address');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(
|
|
||||||
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this address'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -113,7 +88,21 @@
|
|||||||
<CreateAddressDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
<CreateAddressDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddressesTable {data} {openEdit} {openArchive} {formatValue} {relationLabel} {recordName} />
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search addresses..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AddressesTable
|
||||||
|
data={listData}
|
||||||
|
{openEdit}
|
||||||
|
{openArchive}
|
||||||
|
{formatValue}
|
||||||
|
{relationLabel}
|
||||||
|
{recordName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditAddressDialog
|
<EditAddressDialog
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import CreateBookingDialog from './create-booking-dialog.svelte';
|
import CreateBookingDialog from './create-booking-dialog.svelte';
|
||||||
import BookingsTable from './bookings-table.svelte';
|
import BookingsTable from './bookings-table.svelte';
|
||||||
import EditBookingDialog from './edit-booking-dialog.svelte';
|
import EditBookingDialog from './edit-booking-dialog.svelte';
|
||||||
@@ -7,11 +9,21 @@
|
|||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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 { bookingCreateSchema, bookingEditSchema } from '$lib/schemas/bookings.schema';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let createOpen = $state(false);
|
let createOpen = $state(false);
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
@@ -44,48 +56,8 @@
|
|||||||
const { form: editData, enhance: enhanceEdit } = editForm;
|
const { form: editData, enhance: enhanceEdit } = editForm;
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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') {
|
function relationLabel(value: unknown, optionsKey: 'clients' | 'rooms' | 'services') {
|
||||||
if (typeof value !== 'string') return 'Not set';
|
return getRelationLabel(data.options, value, optionsKey);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(record: RecordRow) {
|
function openEdit(record: RecordRow) {
|
||||||
@@ -99,15 +71,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this booking');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(
|
|
||||||
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this booking'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -127,8 +91,15 @@
|
|||||||
<CreateBookingDialog bind:open={createOpen} {data} {createForm} {createData} {enhanceCreate} />
|
<CreateBookingDialog bind:open={createOpen} {data} {createForm} {createData} {enhanceCreate} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search bookings..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
<BookingsTable
|
<BookingsTable
|
||||||
{data}
|
data={listData}
|
||||||
{openEdit}
|
{openEdit}
|
||||||
{openArchive}
|
{openArchive}
|
||||||
{formatValue}
|
{formatValue}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import CreateContactDialog from './create-contact-dialog.svelte';
|
import CreateContactDialog from './create-contact-dialog.svelte';
|
||||||
import ContactsTable from './contacts-table.svelte';
|
import ContactsTable from './contacts-table.svelte';
|
||||||
import EditContactDialog from './edit-contact-dialog.svelte';
|
import EditContactDialog from './edit-contact-dialog.svelte';
|
||||||
@@ -7,11 +9,20 @@
|
|||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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 { contactCreateSchema, contactEditSchema } from '$lib/schemas/contacts.schema';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let createOpen = $state(false);
|
let createOpen = $state(false);
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
@@ -44,36 +55,8 @@
|
|||||||
const { form: editData, enhance: enhanceEdit } = editForm;
|
const { form: editData, enhance: enhanceEdit } = editForm;
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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') {
|
function relationLabel(value: unknown, optionsKey: 'clients') {
|
||||||
if (typeof value !== 'string') return 'Not set';
|
return getRelationLabel(data.options, value, optionsKey);
|
||||||
return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(record: RecordRow) {
|
function openEdit(record: RecordRow) {
|
||||||
@@ -87,15 +70,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this contact');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(
|
|
||||||
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this contact'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -113,7 +88,21 @@
|
|||||||
<CreateContactDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
<CreateContactDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ContactsTable {data} {openEdit} {openArchive} {formatValue} {relationLabel} {recordName} />
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search contacts..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ContactsTable
|
||||||
|
data={listData}
|
||||||
|
{openEdit}
|
||||||
|
{openArchive}
|
||||||
|
{formatValue}
|
||||||
|
{relationLabel}
|
||||||
|
{recordName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditContactDialog
|
<EditContactDialog
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import CreateContractDialog from './create-contract-dialog.svelte';
|
import CreateContractDialog from './create-contract-dialog.svelte';
|
||||||
import ContractsTable from './contracts-table.svelte';
|
import ContractsTable from './contracts-table.svelte';
|
||||||
import EditContractDialog from './edit-contract-dialog.svelte';
|
import EditContractDialog from './edit-contract-dialog.svelte';
|
||||||
@@ -7,11 +9,22 @@
|
|||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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 { contractCreateSchema, contractEditSchema } from '$lib/schemas/contracts.schema';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let createOpen = $state(false);
|
let createOpen = $state(false);
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
@@ -44,53 +57,8 @@
|
|||||||
const { form: editData, enhance: enhanceEdit } = editForm;
|
const { form: editData, enhance: enhanceEdit } = editForm;
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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') {
|
function relationLabel(value: unknown, optionsKey: 'clients') {
|
||||||
if (typeof value !== 'string') return 'Not set';
|
return getRelationLabel(data.options, value, optionsKey);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(record: RecordRow) {
|
function openEdit(record: RecordRow) {
|
||||||
@@ -104,15 +72,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this contract');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(
|
|
||||||
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this contract'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -130,8 +90,15 @@
|
|||||||
<CreateContractDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
<CreateContractDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search contracts..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
<ContractsTable
|
<ContractsTable
|
||||||
{data}
|
data={listData}
|
||||||
{openEdit}
|
{openEdit}
|
||||||
{openArchive}
|
{openArchive}
|
||||||
{formatValue}
|
{formatValue}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import CreateInvoiceDialog from './create-invoice-dialog.svelte';
|
import CreateInvoiceDialog from './create-invoice-dialog.svelte';
|
||||||
import InvoicesTable from './invoices-table.svelte';
|
import InvoicesTable from './invoices-table.svelte';
|
||||||
import EditInvoiceDialog from './edit-invoice-dialog.svelte';
|
import EditInvoiceDialog from './edit-invoice-dialog.svelte';
|
||||||
@@ -7,11 +9,22 @@
|
|||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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 { invoiceCreateSchema, invoiceEditSchema } from '$lib/schemas/invoices.schema';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let createOpen = $state(false);
|
let createOpen = $state(false);
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
@@ -44,53 +57,8 @@
|
|||||||
const { form: editData, enhance: enhanceEdit } = editForm;
|
const { form: editData, enhance: enhanceEdit } = editForm;
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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') {
|
function relationLabel(value: unknown, optionsKey: 'clients') {
|
||||||
if (typeof value !== 'string') return 'Not set';
|
return getRelationLabel(data.options, value, optionsKey);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(record: RecordRow) {
|
function openEdit(record: RecordRow) {
|
||||||
@@ -104,15 +72,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this invoice');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(
|
|
||||||
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this invoice'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -130,8 +90,15 @@
|
|||||||
<CreateInvoiceDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
<CreateInvoiceDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search invoices..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
<InvoicesTable
|
<InvoicesTable
|
||||||
{data}
|
data={listData}
|
||||||
{openEdit}
|
{openEdit}
|
||||||
{openArchive}
|
{openArchive}
|
||||||
{formatValue}
|
{formatValue}
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import ContactsTable from './contacts-table.svelte';
|
import ContactsTable from './contacts-table.svelte';
|
||||||
import ArchiveContactDialog from './archive-contact-dialog.svelte';
|
import ArchiveContactDialog from './archive-contact-dialog.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
|
|
||||||
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
|
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
|
||||||
@@ -23,36 +34,8 @@
|
|||||||
|
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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') {
|
function relationLabel(value: unknown, optionsKey: 'clients') {
|
||||||
if (typeof value !== 'string') return 'Not set';
|
return getRelationLabel(data.options, value, optionsKey);
|
||||||
return data.options[optionsKey].find((option) => option.value === value)?.label ?? value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openArchive(record: RecordRow) {
|
function openArchive(record: RecordRow) {
|
||||||
@@ -61,15 +44,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this contact');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(
|
|
||||||
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this contact'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -85,7 +60,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ContactsTable {data} {openArchive} {formatValue} {relationLabel} {recordName} />
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search contacts..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ContactsTable data={listData} {openArchive} {formatValue} {relationLabel} {recordName} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ArchiveContactDialog
|
<ArchiveContactDialog
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import ContractsTable from './contracts-table.svelte';
|
import ContractsTable from './contracts-table.svelte';
|
||||||
import ArchiveContractDialog from './archive-contract-dialog.svelte';
|
import ArchiveContractDialog from './archive-contract-dialog.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
|
|
||||||
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
|
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
|
||||||
@@ -23,53 +36,8 @@
|
|||||||
|
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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') {
|
function relationLabel(value: unknown, optionsKey: 'clients') {
|
||||||
if (typeof value !== 'string') return 'Not set';
|
return getRelationLabel(data.options, value, optionsKey);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openArchive(record: RecordRow) {
|
function openArchive(record: RecordRow) {
|
||||||
@@ -78,15 +46,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this contract');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(
|
|
||||||
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this contract'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -102,8 +62,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search contracts..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
<ContractsTable
|
<ContractsTable
|
||||||
{data}
|
data={listData}
|
||||||
{openArchive}
|
{openArchive}
|
||||||
{formatValue}
|
{formatValue}
|
||||||
{formatDate}
|
{formatDate}
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import InvoicesTable from './invoices-table.svelte';
|
import InvoicesTable from './invoices-table.svelte';
|
||||||
import ArchiveInvoiceDialog from './archive-invoice-dialog.svelte';
|
import ArchiveInvoiceDialog from './archive-invoice-dialog.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
|
|
||||||
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
|
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
|
||||||
@@ -23,53 +36,8 @@
|
|||||||
|
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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') {
|
function relationLabel(value: unknown, optionsKey: 'clients') {
|
||||||
if (typeof value !== 'string') return 'Not set';
|
return getRelationLabel(data.options, value, optionsKey);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openArchive(record: RecordRow) {
|
function openArchive(record: RecordRow) {
|
||||||
@@ -78,15 +46,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this invoice');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(
|
|
||||||
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this invoice'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -102,8 +62,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search invoices..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
<InvoicesTable
|
<InvoicesTable
|
||||||
{data}
|
data={listData}
|
||||||
{openArchive}
|
{openArchive}
|
||||||
{formatValue}
|
{formatValue}
|
||||||
{formatDate}
|
{formatDate}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import CreateRoomDialog from './create-room-dialog.svelte';
|
import CreateRoomDialog from './create-room-dialog.svelte';
|
||||||
import RoomsTable from './rooms-table.svelte';
|
import RoomsTable from './rooms-table.svelte';
|
||||||
import EditRoomDialog from './edit-room-dialog.svelte';
|
import EditRoomDialog from './edit-room-dialog.svelte';
|
||||||
@@ -7,11 +9,16 @@
|
|||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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 { roomCreateSchema, roomEditSchema } from '$lib/schemas/rooms.schema';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let createOpen = $state(false);
|
let createOpen = $state(false);
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
@@ -44,39 +51,6 @@
|
|||||||
const { form: editData, enhance: enhanceEdit } = editForm;
|
const { form: editData, enhance: enhanceEdit } = editForm;
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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) {
|
function formatRoomType(value: unknown) {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
meeting_room: 'Meeting room',
|
meeting_room: 'Meeting room',
|
||||||
@@ -98,13 +72,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this room');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this room');
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -124,8 +92,15 @@
|
|||||||
<CreateRoomDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
<CreateRoomDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search rooms..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
<RoomsTable
|
<RoomsTable
|
||||||
{data}
|
data={listData}
|
||||||
{openEdit}
|
{openEdit}
|
||||||
{openArchive}
|
{openArchive}
|
||||||
{formatValue}
|
{formatValue}
|
||||||
|
|||||||
@@ -42,8 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pricing(record: any) {
|
function pricing(record: any) {
|
||||||
if (record.type === 'private_office')
|
if (record.type === 'private_office') return `${formatMoney(record.pricePerMonthGbp)} / month`;
|
||||||
return `${formatMoney(record.pricePerMonthGbp)} / month`;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`${formatMoney(record.internalPricePerHourGbp)} internal / hour`,
|
`${formatMoney(record.internalPricePerHourGbp)} internal / hour`,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createListSearch } from '$lib/list-search.svelte';
|
||||||
|
import ListSearch from '$lib/components/list-search.svelte';
|
||||||
import CreateServiceDialog from './create-service-dialog.svelte';
|
import CreateServiceDialog from './create-service-dialog.svelte';
|
||||||
import ServicesTable from './services-table.svelte';
|
import ServicesTable from './services-table.svelte';
|
||||||
import EditServiceDialog from './edit-service-dialog.svelte';
|
import EditServiceDialog from './edit-service-dialog.svelte';
|
||||||
@@ -7,11 +9,16 @@
|
|||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
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 { serviceCreateSchema, serviceEditSchema } from '$lib/schemas/services.schema';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
type RecordRow = PageData['records'][number];
|
type RecordRow = PageData['records'][number];
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const listSearch = createListSearch(() => data.records);
|
||||||
|
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||||
|
|
||||||
let createOpen = $state(false);
|
let createOpen = $state(false);
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let archivingId = $state<string | null>(null);
|
let archivingId = $state<string | null>(null);
|
||||||
@@ -44,38 +51,6 @@
|
|||||||
const { form: editData, enhance: enhanceEdit } = editForm;
|
const { form: editData, enhance: enhanceEdit } = editForm;
|
||||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||||
|
|
||||||
function handleFormToast(
|
|
||||||
form: { valid: boolean; message?: string; errors: Record<string, unknown> },
|
|
||||||
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<string, unknown>) {
|
|
||||||
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) {
|
function openEdit(record: RecordRow) {
|
||||||
editForm.reset({ data: record.formValues as never });
|
editForm.reset({ data: record.formValues as never });
|
||||||
editingId = record.id;
|
editingId = record.id;
|
||||||
@@ -87,15 +62,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordName(record: Partial<RecordRow> | undefined) {
|
function recordName(record: Partial<RecordRow> | undefined) {
|
||||||
const item = record as Partial<RecordRow> & {
|
return getRecordName(record, 'this service');
|
||||||
name?: unknown;
|
|
||||||
title?: unknown;
|
|
||||||
invoiceNumber?: unknown;
|
|
||||||
label?: unknown;
|
|
||||||
};
|
|
||||||
return String(
|
|
||||||
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this service'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -113,7 +80,21 @@
|
|||||||
<CreateServiceDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
<CreateServiceDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ServicesTable {data} {openEdit} {openArchive} {formatValue} {formatMoney} {recordName} />
|
<ListSearch
|
||||||
|
bind:value={listSearch.params.q}
|
||||||
|
placeholder="Search services..."
|
||||||
|
totalCount={data.records.length}
|
||||||
|
resultCount={listSearch.filtered.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ServicesTable
|
||||||
|
data={listData}
|
||||||
|
{openEdit}
|
||||||
|
{openArchive}
|
||||||
|
{formatValue}
|
||||||
|
{formatMoney}
|
||||||
|
{recordName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditServiceDialog
|
<EditServiceDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user