feat: add billing and organization workflows
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
<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 ContractsTable from './contracts-table.svelte';
|
||||
import EditContractDialog from './edit-contract-dialog.svelte';
|
||||
@@ -10,21 +8,14 @@
|
||||
import { zod4Client } from 'sveltekit-superforms/adapters';
|
||||
import { archiveSchema } from '$lib/schemas/shared.schema';
|
||||
import {
|
||||
formatValue,
|
||||
formatMoney,
|
||||
formatDate,
|
||||
recordName as getRecordName,
|
||||
relationLabel as getRelationLabel
|
||||
} from '$lib/record-utils';
|
||||
import { handleFormToast } from '$lib/form-feedback';
|
||||
import { contractCreateSchema, contractEditSchema } from '$lib/schemas/contracts.schema';
|
||||
contractCreateSchema,
|
||||
contractEditSchema,
|
||||
contractTransitionSchema
|
||||
} from '$lib/schemas/contracts.schema';
|
||||
import type { PageData } from './$types';
|
||||
type RecordRow = PageData['records'][number];
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const listSearch = createListSearch(() => data.records);
|
||||
const listData = $derived({ ...data, records: listSearch.filtered });
|
||||
|
||||
let createOpen = $state(false);
|
||||
let editingId = $state<string | null>(null);
|
||||
let archivingId = $state<string | null>(null);
|
||||
@@ -52,17 +43,67 @@
|
||||
onUpdated: ({ form }) => handleFormToast(form, 'contracts-archive', () => (archivingId = null)),
|
||||
onError: ({ result }) => toast.error(result.error.message, { id: 'contracts-archive' })
|
||||
});
|
||||
// svelte-ignore state_referenced_locally
|
||||
const transitionForm = superForm(data.transitionForm, {
|
||||
validators: zod4Client(contractTransitionSchema),
|
||||
resetForm: false,
|
||||
onUpdated: ({ form }) =>
|
||||
handleFormToast(form, 'contracts-transition', () => (editingId = null)),
|
||||
onError: ({ result }) => toast.error(result.error.message, { id: 'contracts-transition' })
|
||||
});
|
||||
|
||||
const { form: createData, enhance: enhanceCreate } = createForm;
|
||||
const { form: editData, enhance: enhanceEdit } = editForm;
|
||||
const { form: archiveData, enhance: enhanceArchive } = archiveForm;
|
||||
const { enhance: enhanceTransition } = transitionForm;
|
||||
|
||||
function relationLabel(value: unknown, optionsKey: 'clients') {
|
||||
return getRelationLabel(data.options, value, optionsKey);
|
||||
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 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 });
|
||||
transitionForm.reset({
|
||||
data: {
|
||||
id: record.id,
|
||||
targetStatus: record.status === 'active' ? 'expired' : 'active'
|
||||
}
|
||||
});
|
||||
editingId = record.id;
|
||||
}
|
||||
|
||||
@@ -72,7 +113,7 @@
|
||||
}
|
||||
|
||||
function recordName(record: Partial<RecordRow> | undefined) {
|
||||
return getRecordName(record, 'this contract');
|
||||
return String(record?.roomName ?? record?.serviceName ?? 'this contract');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -87,33 +128,25 @@
|
||||
<p class="text-sm text-muted-foreground">Manage client agreements and commercial terms.</p>
|
||||
</div>
|
||||
|
||||
<CreateContractDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
|
||||
<CreateContractDialog
|
||||
bind:open={createOpen}
|
||||
options={data.options}
|
||||
{createForm}
|
||||
{createData}
|
||||
{enhanceCreate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ListSearch
|
||||
bind:value={listSearch.params.q}
|
||||
placeholder="Search contracts..."
|
||||
totalCount={data.records.length}
|
||||
resultCount={listSearch.filtered.length}
|
||||
/>
|
||||
|
||||
<ContractsTable
|
||||
data={listData}
|
||||
{openEdit}
|
||||
{openArchive}
|
||||
{formatValue}
|
||||
{formatDate}
|
||||
{formatMoney}
|
||||
{relationLabel}
|
||||
{recordName}
|
||||
/>
|
||||
<ContractsTable {data} {openEdit} {openArchive} {formatValue} {formatDate} {recordName} />
|
||||
</div>
|
||||
|
||||
<EditContractDialog
|
||||
{editingRecord}
|
||||
options={data.options}
|
||||
{editForm}
|
||||
{editData}
|
||||
{enhanceEdit}
|
||||
{enhanceTransition}
|
||||
onClose={() => (editingId = null)}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user