123 lines
3.6 KiB
Svelte
123 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
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 type { PageData } from './$types';
|
|
type RecordRow = PageData['records'][number];
|
|
let { data }: { data: PageData } = $props();
|
|
|
|
let archivingId = $state<string | null>(null);
|
|
|
|
const archivingRecord = $derived(data.records.find((record) => record.id === archivingId));
|
|
|
|
// svelte-ignore state_referenced_locally
|
|
const archiveForm = superForm(data.archiveForm, {
|
|
validators: zod4Client(archiveSchema),
|
|
resetForm: false,
|
|
onUpdated: ({ form }) => handleFormToast(form, 'contracts-archive', () => (archivingId = null)),
|
|
onError: ({ result }) => toast.error(result.error.message, { id: 'contracts-archive' })
|
|
});
|
|
|
|
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') {
|
|
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);
|
|
}
|
|
|
|
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 contract'
|
|
);
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Contracts | 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">Contracts</h1>
|
|
<p class="text-sm text-muted-foreground">Manage client agreements and commercial terms.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<ContractsTable
|
|
{data}
|
|
{openArchive}
|
|
{formatValue}
|
|
{formatDate}
|
|
{formatMoney}
|
|
{relationLabel}
|
|
{recordName}
|
|
/>
|
|
</div>
|
|
|
|
<ArchiveContractDialog
|
|
{archivingRecord}
|
|
{archiveData}
|
|
{enhanceArchive}
|
|
{recordName}
|
|
onClose={() => (archivingId = null)}
|
|
/>
|