Files
clearity/src/routes/dashboard/clients/[id]/bookings/+page.svelte
T

157 lines
4.9 KiB
Svelte

<script lang="ts">
import CreateBookingDialog from './create-booking-dialog.svelte';
import BookingsTable from './bookings-table.svelte';
import EditBookingDialog from './edit-booking-dialog.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 { bookingCreateSchema, bookingEditSchema } from '$lib/schemas/bookings.schema';
import type { PageData } from './$types';
type RecordRow = PageData['records'][number];
let { data }: { data: PageData } = $props();
let createOpen = $state(false);
let editingId = $state<string | null>(null);
let archivingId = $state<string | null>(null);
const editingRecord = $derived(data.records.find((record) => record.id === editingId));
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
// svelte-ignore state_referenced_locally
const createForm = superForm(data.createForm, {
validators: zod4Client(bookingCreateSchema),
onUpdated: ({ form }) => handleFormToast(form, 'bookings-create', () => (createOpen = false)),
onError: ({ result }) => toast.error(result.error.message, { id: 'bookings-create' })
});
// svelte-ignore state_referenced_locally
const editForm = superForm(data.editForm, {
validators: zod4Client(bookingEditSchema),
resetForm: false,
onUpdated: ({ form }) => handleFormToast(form, 'bookings-edit', () => (editingId = null)),
onError: ({ result }) => toast.error(result.error.message, { id: 'bookings-edit' })
});
// svelte-ignore state_referenced_locally
const archiveForm = superForm(data.archiveForm, {
validators: zod4Client(archiveSchema),
resetForm: false,
onUpdated: ({ form }) => handleFormToast(form, 'bookings-archive', () => (archivingId = null)),
onError: ({ result }) => toast.error(result.error.message, { id: 'bookings-archive' })
});
const { form: createData, enhance: enhanceCreate } = createForm;
const { form: editData, enhance: enhanceEdit } = editForm;
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') {
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);
}
function openEdit(record: RecordRow) {
editForm.reset({ data: record.formValues as never });
editingId = record.id;
}
function openArchive(record: RecordRow) {
archiveForm.reset({ data: { id: record.id } });
archivingId = record.id;
}
function recordName(record: Partial<RecordRow> | undefined) {
const item = record as Partial<RecordRow> & {
name?: unknown;
title?: unknown;
invoiceNumber?: unknown;
label?: unknown;
};
return String(
item?.name ?? item?.title ?? item?.invoiceNumber ?? item?.label ?? 'this booking'
);
}
</script>
<svelte:head>
<title>Bookings | Clearity</title>
</svelte:head>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold tracking-tight">Bookings</h1>
<p class="text-sm text-muted-foreground">
Track client room bookings and service reservations.
</p>
</div>
<CreateBookingDialog bind:open={createOpen} {data} {createForm} {createData} {enhanceCreate} />
</div>
<BookingsTable
{data}
{openEdit}
{openArchive}
{formatValue}
{formatDate}
{relationLabel}
{recordName}
/>
</div>
<EditBookingDialog
{editingRecord}
{data}
{editForm}
{editData}
{enhanceEdit}
onClose={() => (editingId = null)}
/>
<ArchiveBookingDialog
{archivingRecord}
{archiveData}
{enhanceArchive}
{recordName}
onClose={() => (archivingId = null)}
/>