feat: add client workspace management
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
<script lang="ts">
|
||||
import CreateClientDialog from './create-client-dialog.svelte';
|
||||
import ClientsTable from './clients-table.svelte';
|
||||
import EditClientDialog from './edit-client-dialog.svelte';
|
||||
import ArchiveClientDialog from './archive-client-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 { clientEditSchema, clientOnboardingCreateSchema } from '$lib/schemas/clients.schema';
|
||||
import type { PageData } from './$types';
|
||||
type RecordRow = PageData['records'][number];
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createOpen = $state(false);
|
||||
let createStep = $state(0);
|
||||
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(clientOnboardingCreateSchema),
|
||||
onUpdated: ({ form }) =>
|
||||
handleFormToast(form, 'clients-create', () => {
|
||||
createOpen = false;
|
||||
createStep = 0;
|
||||
}),
|
||||
onError: ({ result }) => toast.error(result.error.message, { id: 'clients-create' })
|
||||
});
|
||||
// svelte-ignore state_referenced_locally
|
||||
const editForm = superForm(data.editForm, {
|
||||
validators: zod4Client(clientEditSchema),
|
||||
resetForm: false,
|
||||
onUpdated: ({ form }) => handleFormToast(form, 'clients-edit', () => (editingId = null)),
|
||||
onError: ({ result }) => toast.error(result.error.message, { id: 'clients-edit' })
|
||||
});
|
||||
// svelte-ignore state_referenced_locally
|
||||
const archiveForm = superForm(data.archiveForm, {
|
||||
validators: zod4Client(archiveSchema),
|
||||
resetForm: false,
|
||||
onUpdated: ({ form }) => handleFormToast(form, 'clients-archive', () => (archivingId = null)),
|
||||
onError: ({ result }) => toast.error(result.error.message, { id: 'clients-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 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 client');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Clients | 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">Clients</h1>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Manage businesses, sports organisations, and individual clients.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CreateClientDialog
|
||||
bind:open={createOpen}
|
||||
bind:createStep
|
||||
{createForm}
|
||||
{createData}
|
||||
{enhanceCreate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ClientsTable {data} {openEdit} {openArchive} {formatValue} {recordName} />
|
||||
</div>
|
||||
|
||||
<EditClientDialog
|
||||
{editingRecord}
|
||||
{editForm}
|
||||
{editData}
|
||||
{enhanceEdit}
|
||||
onClose={() => (editingId = null)}
|
||||
/>
|
||||
|
||||
<ArchiveClientDialog
|
||||
{archivingRecord}
|
||||
{archiveData}
|
||||
{enhanceArchive}
|
||||
{recordName}
|
||||
onClose={() => (archivingId = null)}
|
||||
/>
|
||||
Reference in New Issue
Block a user