continue ordertest create & edit

This commit is contained in:
faiztyanirh 2026-04-22 17:18:56 +07:00
parent 84a14c118f
commit f8af8c21e6
14 changed files with 341 additions and 94 deletions

View File

@ -46,7 +46,6 @@ export async function searchWithParams(endpoint, searchQuery) {
: `${API.BASE_URL}${endpoint}`; : `${API.BASE_URL}${endpoint}`;
const res = await fetch(url); const res = await fetch(url);
const data = await res.json(); const data = await res.json();
console.log(url);
return data.data || []; return data.data || [];
} catch (err) { } catch (err) {
console.error('Search Error:', err); console.error('Search Error:', err);

View File

@ -0,0 +1,12 @@
export function useOrderForm(formState) {
let uploadErrors = $state({});
let isChecking = $state({});
let hasErrors = $derived(
Object.values(formState.errors).some(value => value !== null)
);
return {
get hasErrors() { return hasErrors },
}
}

View File

@ -385,7 +385,6 @@
reset: () => { tempMap = []; idCounter = 0; editingId = null; }, reset: () => { tempMap = []; idCounter = 0; editingId = null; },
}); });
}); });
$inspect(testMapDetailFormState.form)
</script> </script>
<FormPageContainer title="Edit Test Map" {primaryAction} {secondaryActions}> <FormPageContainer title="Edit Test Map" {primaryAction} {secondaryActions}>

View File

@ -43,8 +43,12 @@ export const detailSections = [
{ {
class: "grid grid-cols-2 gap-4 items-center", class: "grid grid-cols-2 gap-4 items-center",
fields: [ fields: [
{ key: "OrderID", label: "Order ID" },
{ key: "PlacerID", label: "Host ID" }, { key: "PlacerID", label: "Host ID" },
{ key: "PriorityLabel", label: "Priority" },
{ key: "TrnDate", label: "Transaction Date" },
{ key: "EffDate", label: "Effective Date" },
{ key: "Comment", label: "Comment" },
{ key: "Tests", label: "Tests", fullWidth: true },
] ]
}, },
]; ];
@ -56,7 +60,9 @@ export function orderTestActions(masterDetail, selectedPatient, selectedVisit) {
label: 'Add Order', label: 'Add Order',
onClick: () => masterDetail.enterCreate({ onClick: () => masterDetail.enterCreate({
PatientID: selectedPatient?.PatientID, PatientID: selectedPatient?.PatientID,
InternalPID: selectedPatient?.InternalPID PatientName: selectedPatient?.FullName,
InternalPID: selectedPatient?.InternalPID,
PVADTID: selectedVisit?.PVADTID,
}), }),
disabled: !selectedPatient, disabled: !selectedPatient,
}, },

View File

@ -3,7 +3,32 @@ import EraserIcon from "@lucide/svelte/icons/eraser";
import { z } from "zod"; import { z } from "zod";
import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings"; import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings";
export const orderTestSchema = z.object({}); export const orderTestSchema = z.object({
Tests: z.array(
z.object({
id: z.number(),
TestSiteCode: z.string(),
TestSiteName: z.string()
})
)
.min(1, " "),
})
.superRefine((data, ctx) => {
if (!data.Tests) return;
const values = data.Tests.map((m) => m.testCode).filter(Boolean);
const duplicates = values.filter((v, i) => values.indexOf(v) !== i);
if (duplicates.length) {
const uniqueDuplicates = [...new Set(duplicates)];
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Duplicate test : ${uniqueDuplicates.join(', ')}`,
path: ['Tests']
});
}
});
export const orderTestInitialForm = { export const orderTestInitialForm = {
InternalOID: '', InternalOID: '',
@ -18,10 +43,12 @@ export const orderTestInitialForm = {
EffDate: '', EffDate: '',
Comment: '', Comment: '',
OrderAtt: '', OrderAtt: '',
Tests: '', Tests: [],
}; };
export const orderTestDefaultErrors = {}; export const orderTestDefaultErrors = {
Tests: '',
};
export const orderTestFormFields = [ export const orderTestFormFields = [
{ {
@ -72,14 +99,14 @@ export const orderTestFormFields = [
key: "TrnDate", key: "TrnDate",
label: "Transaction Date", label: "Transaction Date",
required: false, required: false,
type: "date", type: "datetime",
allowFuture: true allowFuture: true
}, },
{ {
key: "EffDate", key: "EffDate",
label: "Effective Date", label: "Effective Date",
required: false, required: false,
type: "date", type: "datetime",
allowFuture: true allowFuture: true
}, },
] ]
@ -117,11 +144,12 @@ export const orderTestFormFields = [
{ {
key: "Tests", key: "Tests",
label: "Search Test", label: "Search Test",
required: false, required: true,
type: "tests", type: "tests",
optionsEndpoint: `${API.BASE_URL}${API.TEST}`, optionsEndpoint: `${API.BASE_URL}${API.TEST}`,
valueKey: 'TestSiteCode', valueKey: 'TestSiteCode',
labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`, labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`,
validateOn: ['input']
}, },
] ]
}, },
@ -138,3 +166,16 @@ export function getOrderTestFormActions(handlers) {
}, },
]; ];
} }
export function buildOrderTestPayload(mainForm){
const { InternalOID, PatientID, PatientName, Tests, ...rest } = mainForm;
let payload = {
...rest,
// Tests: Tests.map(test => ({
// TestSiteID: test.testSiteID,
// }))
}
return cleanEmptyStrings(payload)
}

View File

@ -1,9 +1,10 @@
<script> <script>
import { usePatientForm } from "$lib/components/composable/use-patient-form.svelte"; import { useOrderForm } from "$lib/components/composable/use-order-form.svelte";
import FormPageContainer from "$lib/components/patient/reusable/form-page-container.svelte"; import FormPageContainer from "$lib/components/reusable/form/form-page-container.svelte";
import OrderFormRenderer from "$lib/components/reusable/form/order-form-renderer.svelte"; import OrderFormRenderer from "$lib/components/reusable/form/order-form-renderer.svelte";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte"; import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
import { buildOrderTestPayload } from "$lib/components/order/ordertest/config/ordertest-form-config";
let props = $props(); let props = $props();
@ -11,7 +12,7 @@
const { formState } = masterDetail; const { formState } = masterDetail;
const helpers = usePatientForm(formState, schema); const helpers = useOrderForm(formState);
const handlers = { const handlers = {
clearForm: () => { clearForm: () => {
@ -24,15 +25,21 @@
let showConfirm = $state(false); let showConfirm = $state(false);
async function handleSave() { async function handleSave() {
// const payload = buildPayload(formState.form); const payload = buildOrderTestPayload(formState.form);
console.log(payload);
// const result = await formState.save(masterDetail.mode, payload); // const result = await formState.save(masterDetail.mode, payload);
// console.log(payload); // if (result.status === 'success') {
// toast('Visit Created!'); // toast('Order Test Created!');
// masterDetail?.exitForm(true); // masterDetail?.exitForm(true);
// } else {
// console.error('Failed to save order test');
// const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save order test';
// toast.error(errorMessages)
// }
} }
$inspect(formState.form)
const primaryAction = $derived({ const primaryAction = $derived({
label: 'Save', label: 'Save',
onClick: handleSave, onClick: handleSave,
@ -41,18 +48,9 @@
}); });
const secondaryActions = []; const secondaryActions = [];
$inspect(formState.form)
// $effect(() => {
// if (masterDetail.form?.PatientID) {
// formState.setForm({
// ...formState.form,
// ...masterDetail.form
// });
// }
// });
</script> </script>
<FormPageContainer title="Create Order for {formState.form?.PatientID}" {primaryAction} {secondaryActions} {actions}> <FormPageContainer title="Create Order for {formState.form?.PatientID} - {formState.form?.PatientName}" {primaryAction} {secondaryActions} {actions}>
<OrderFormRenderer <OrderFormRenderer
{formState} {formState}
formFields={formFields} formFields={formFields}

View File

@ -0,0 +1,69 @@
<script>
import { useOrderForm } from "$lib/components/composable/use-order-form.svelte";
import FormPageContainer from "$lib/components/reusable/form/form-page-container.svelte";
import OrderFormRenderer from "$lib/components/reusable/form/order-form-renderer.svelte";
import { toast } from "svelte-sonner";
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
import { buildOrderTestPayload } from "$lib/components/order/ordertest/config/ordertest-form-config";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
const { formState } = masterDetail;
const helpers = useOrderForm(formState);
let showConfirm = $state(false);
async function handleEdit() {
console.log('edit');
// const payload = buildOrderTestPayload(formState.form);
// console.log(payload);
// const result = await formState.save(masterDetail.mode, payload);
// if (result.status === 'success') {
// toast('Order Test Created!');
// masterDetail?.exitForm(true);
// } else {
// console.error('Failed to save order test');
// const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save order test';
// toast.error(errorMessages)
// }
}
const primaryAction = $derived({
label: 'Save',
onClick: handleEdit,
disabled: helpers.hasErrors || formState.isSaving.current,
loading: formState.isSaving.current
});
const secondaryActions = [];
// $effect(() => {
// const maxId = formState.form.Tests.reduce((max, row) => {
// const rowId = typeof row.id === 'number' ? row.id : 0;
// return rowId > max ? rowId : max;
// }, 0);
// if (maxId > idCounter) {
// idCounter = maxId;
// }
// });
$inspect(formState.form.Tests)
</script>
<FormPageContainer title="Edit Order for {formState.form?.PatientID} - {formState.form?.PatientName}" {primaryAction} {secondaryActions}>
<OrderFormRenderer
{formState}
formFields={formFields}
mode="create"
/>
</FormPageContainer>
<ReusableAlertDialog
bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit}
/>

View File

@ -22,7 +22,7 @@
let selectedVisit = $state(null); let selectedVisit = $state(null);
let isLoading = $state(false); let isLoading = $state(false);
let searchData = $state([]); let searchData = $state([]);
$inspect(selectedPatient)
const search = useSearch(searchFields, searchParam); const search = useSearch(searchFields, searchParam);
let actions = $derived.by(() => { let actions = $derived.by(() => {
@ -85,7 +85,7 @@ $inspect(selectedPatient)
onclick={() => props.masterDetail.isFormMode && props.masterDetail.exitForm()} onclick={() => props.masterDetail.isFormMode && props.masterDetail.exitForm()}
onkeydown={(e) => e.key === 'Enter' && props.masterDetail.isFormMode && props.masterDetail.exitForm()} onkeydown={(e) => e.key === 'Enter' && props.masterDetail.isFormMode && props.masterDetail.exitForm()}
class={` class={`
${props.masterDetail.isMobile ? "w-full" : props.masterDetail.isFormMode ? "w-[3%] cursor-pointer" : "w-[35%]"} ${props.masterDetail.isMobile ? "w-full" : props.masterDetail.isFormMode ? "w-[3%] cursor-pointer" : "w-[55%]"}
transition-all duration-300 flex flex-col items-center p-2 h-full overflow-y-auto transition-all duration-300 flex flex-col items-center p-2 h-full overflow-y-auto
`} `}
> >
@ -136,7 +136,7 @@ $inspect(selectedPatient)
{/if} {/if}
<div class="flex-1 w-full h-full"> <div class="flex-1 w-full h-full">
{#if searchData?.length > 0} {#if searchData?.length > 0}
<ReusableDataTable data={searchData} columns={orderTestColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="InternalOID" offset="7"/> <ReusableDataTable data={searchData} columns={orderTestColumns} handleRowClick={props.masterDetail.select} {activeRowId} rowIdKey="InternalOID" offset="8"/>
{:else} {:else}
<div class="flex h-full"> <div class="flex h-full">
<!-- <ReusableEmpty icon={ClipboardXIcon} desc="Try searching from search parameters"/> --> <!-- <ReusableEmpty icon={ClipboardXIcon} desc="Try searching from search parameters"/> -->

View File

@ -3,6 +3,8 @@
import { detailSections, viewActions } from "$lib/components/order/ordertest/config/ordertest-config"; import { detailSections, viewActions } from "$lib/components/order/ordertest/config/ordertest-config";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte"; import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte"; import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import * as Table from "$lib/components/ui/table/index.js";
import { Spinner } from "$lib/components/ui/spinner/index.js";
let props = $props(); let props = $props();
@ -28,8 +30,45 @@
return field.parentKey ? order[field.parentKey]?.[field.key] : order[field.key]; return field.parentKey ? order[field.parentKey]?.[field.key] : order[field.key];
} }
</script> </script>
{#snippet DetailsTable({ value, label })}
<div class="space-y-1.5 w-full">
<dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{label}
</dt>
<dd>
{#if value && Array.isArray(value) && value.length > 0}
<div class="border rounded-md">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Test Code</Table.Head>
<Table.Head>Test Name</Table.Head>
<Table.Head>Discipline</Table.Head>
<Table.Head>Create Date</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each value as row, i}
<Table.Row>
<Table.Cell>{row.TestSiteCode ?? '-'}</Table.Cell>
<Table.Cell>{row.TestSiteName ?? '-'}</Table.Cell>
<Table.Cell>{row.Discipline.DisciplineName ?? '-'}</Table.Cell>
<Table.Cell>{formatUTCDate(row.CreateDate) ?? '-'}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
{:else}
<span class="text-sm font-medium">-</span>
{/if}
</dd>
</div>
{/snippet}
{#snippet Fieldset({ value, label, isUTCDate = false })} {#snippet Fieldset({ value, label, isUTCDate = false })}
<div class="space-y-1.5"> <div class="space-y-1.5">
<dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider"> <dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
@ -45,7 +84,11 @@
</div> </div>
{/snippet} {/snippet}
{#if masterDetail.selectedItem} {#if masterDetail.isLoadingDetail}
<div class="h-full w-full flex items-center justify-center">
<Spinner class="size-6" />
</div>
{:else if masterDetail.selectedItem}
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full"> <div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper <TopbarWrapper
title={masterDetail.selectedItem.data.OrderID} title={masterDetail.selectedItem.data.OrderID}
@ -53,14 +96,29 @@
/> />
<div class="flex-1 min-h-0 overflow-y-auto space-y-4"> <div class="flex-1 min-h-0 overflow-y-auto space-y-4">
{#each detailSections as section} {#each detailSections as section}
<div class="p-4"> <div class="flex flex-col px-4 py-2 gap-2">
<div class={section.class}> <div class="{section.class} w-full">
{#each section.fields as field} {#each section.fields as field}
{@render Fieldset({ {#if field.fullWidth}
label: field.label, <div class="col-span-2 mt-2">
value: getFieldValue(field), {#if field.key === "Tests"}
isUTCDate: field.isUTCDate {@render DetailsTable({ label: field.label, value: getFieldValue(field) })}
})} {:else}
{@render Fieldset({ label: field.label, value: getFieldValue(field), isUTCDate: field.isUTCDate })}
{/if}
</div>
{:else if field.key === "Tests"}
{@render DetailsTable({
label: field.label,
value: getFieldValue(field),
})}
{:else}
{@render Fieldset({
label: field.label,
value: getFieldValue(field),
isUTCDate: field.isUTCDate
})}
{/if}
{/each} {/each}
</div> </div>
</div> </div>

View File

@ -7,4 +7,16 @@ export const orderTestColumns = [
accessorKey: "PlacerID", accessorKey: "PlacerID",
header: "Host ID", header: "Host ID",
}, },
{
accessorKey: "TrnDate",
header: "Transaction Date",
},
{
accessorKey: "EffDate",
header: "Effective Date",
},
{
accessorKey: "Priority",
header: "Priority",
},
]; ];

View File

@ -20,7 +20,6 @@
<div class="flex-1 min-h-0 overflow-y-auto p-2"> <div class="flex-1 min-h-0 overflow-y-auto p-2">
{@render children()} {@render children()}
</div> </div>
<!-- <div class="mt-auto flex justify-end items-center pt-2"> -->
<div class="shrink-0 border-t pt-2 flex justify-end items-center"> <div class="shrink-0 border-t pt-2 flex justify-end items-center">
<Button <Button
size="sm" size="sm"

View File

@ -7,7 +7,7 @@
import { Input } from '$lib/components/ui/input/index.js'; import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from '$lib/components/ui/label/index.js';
import { Spinner } from '$lib/components/ui/spinner/index.js'; import { Spinner } from '$lib/components/ui/spinner/index.js';
import ReusableCalendar from '$lib/components/reusable/reusable-calendar.svelte'; import ReusableCalendarTimepicker from "$lib/components/reusable/reusable-calendar-timepicker.svelte";
import { Badge } from '$lib/components/ui/badge/index.js'; import { Badge } from '$lib/components/ui/badge/index.js';
import * as InputGroup from '$lib/components/ui/input-group/index.js'; import * as InputGroup from '$lib/components/ui/input-group/index.js';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'; import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
@ -33,9 +33,12 @@
} = $props(); } = $props();
let searchQuery = $state({}); let searchQuery = $state({});
const leftGroups = $derived([formFields[0]]); const leftGroups = $derived([formFields[0]]);
const rightGroups = $derived(formFields.slice(1)); const rightGroups = $derived(formFields.slice(1));
let selectedTest = $state(null);
let selectedCodes = $derived(
new Set((formState.form.Tests ?? []).map(t => t.TestSiteCode))
);
function getFilteredOptions(key) { function getFilteredOptions(key) {
const query = searchQuery[key] || ''; const query = searchQuery[key] || '';
@ -72,9 +75,36 @@
} }
} }
function addTest() {
const testCode = selectedTest?.value;
const testName = selectedTest?.rawItem?.TestSiteName;
const testSiteID = selectedTest?.rawItem?.TestSiteID;
if (!testCode) return;
const exists = formState.form.Tests?.some(t => t.testCode === testCode);
if (exists) {
formState.errors.Tests = 'Test already added';
return;
}
formState.form.Tests = [
...(formState.form.Tests ?? []),
{ id: Date.now(), TestSiteCode: testCode, TestSiteName: testName, TestSiteID: testSiteID }
];
selectedTest = null;
formState.validateField?.('Tests', formState.form.Tests, false);
}
function removeTest(id) {
formState.form.Tests = formState.form.Tests.filter((t) => t.id !== id);
formState.validateField?.('Tests', formState.form.Tests, false);
}
$effect(() => { $effect(() => {
initializeDefaultValues(); initializeDefaultValues();
}); });
</script> </script>
{#snippet Fieldset({ {#snippet Fieldset({
@ -110,7 +140,6 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class="relative flex flex-col items-start w-full"> <div class="relative flex flex-col items-start w-full">
{#if type === 'text'} {#if type === 'text'}
<Input <Input
@ -142,6 +171,9 @@
bind:value={formState.form[key]} bind:value={formState.form[key]}
onValueChange={(val) => { onValueChange={(val) => {
formState.form[key] = val; formState.form[key] = val;
if (validateOn?.includes('input')) {
formState.validateField?.(key, formState.form[key], false);
}
}} }}
onOpenChange={(open) => { onOpenChange={(open) => {
if (open) { if (open) {
@ -184,30 +216,31 @@
{/if} {/if}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
{:else if type === 'date'} {:else if type === "datetime"}
<ReusableCalendar <ReusableCalendarTimepicker
bind:value={formState.form[key]} bind:value={formState.form[key]}
parentFunction={(dateStr) => { parentFunction={(val) => {
formState.form[key] = dateStr; formState.form[key] = val;
if (validateOn?.includes('input')) { if (validateOn?.includes("input")) {
formState.validateField(key, dateStr, false); formState.validateField(key, val, false);
} }
}} }}
allowFuture={allowFuture} allowFuture={allowFuture}
/> />
{:else if type === "fileupload"} {:else if type === "fileupload"}
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<ReusableUpload attachments={formState.form[key]} /> <ReusableUpload attachments={formState.form[key]} />
</div> </div>
{:else if type === "tests"} {:else if type === "tests"}
{@const selectedLabel = {@const selectedLabel = selectedTest?.label || 'Choose'}
formState.selectOptions?.[key]?.find((opt) => opt.value === formState.form[key])?.label ||
'Choose'}
{@const filteredOptions = getFilteredOptions(key)} {@const filteredOptions = getFilteredOptions(key)}
<div class="flex gap-2 w-full"> <div class="flex gap-2 w-full">
<div class="flex-1"> <div class="flex-1">
<Select.Root <Select.Root
type="single" type="single"
onValueChange={(val) => {
selectedTest = filteredOptions.find((opt) => opt.value === val) ?? null;
}}
onOpenChange={(open) => { onOpenChange={(open) => {
if (open && optionsEndpoint) { if (open && optionsEndpoint) {
formState.fetchOptions?.( formState.fetchOptions?.(
@ -236,58 +269,70 @@
<Select.Item value="">- None -</Select.Item> <Select.Item value="">- None -</Select.Item>
{/if} {/if}
{#each filteredOptions as option} {#each filteredOptions as option}
<Select.Item value={option.value}> <Select.Item
{option.label} value={option.value}
</Select.Item> disabled={selectedCodes.has(option.value)}
>
{option.label}
</Select.Item>
{/each} {/each}
{/if} {/if}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</div> </div>
<Button> <Button onclick={addTest}>
<PlusIcon class="size-4" /> <PlusIcon class="size-4" />
Add Test Add Test
</Button> </Button>
</div> </div>
<Table.Root> <Table.Root class="mt-3">
<Table.Header> <Table.Header>
<Table.Row class="hover:bg-transparent"> <Table.Row class="hover:bg-transparent">
<Table.Head>No</Table.Head> <Table.Head class="w-[40px]">No</Table.Head>
<Table.Head>Test Name</Table.Head> <Table.Head>Test Name</Table.Head>
<Table.Head>Discipline</Table.Head> <Table.Head>Test Code</Table.Head>
<Table.Head>Container</Table.Head>
<Table.Head class="w-[40px]"></Table.Head> <Table.Head class="w-[40px]"></Table.Head>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
<Table.Row {#if formState.form.Tests.length === 0}
class="cursor-pointer hover:bg-muted/50" <Table.Row>
> <Table.Cell colspan={4} class="text-center text-muted-foreground py-6">
<Table.Cell>1</Table.Cell> No data. Add a test from Search Test above.
<Table.Cell>Hematologi Rutin</Table.Cell> </Table.Cell>
<Table.Cell>Hematologi</Table.Cell> </Table.Row>
<Table.Cell>EDTA</Table.Cell> {:else}
<Table.Cell> {#each formState.form.Tests as test, index (test.id)}
<div class="flex gap-1"> <Table.Row
<Tooltip.Provider> class="cursor-pointer hover:bg-muted/50"
<Tooltip.Root> >
<Tooltip.Trigger> <Table.Cell>{index + 1}</Table.Cell>
<Button <Table.Cell>{test.TestSiteName}</Table.Cell>
size="icon" <Table.Cell>{test.TestSiteCode}</Table.Cell>
variant="outline" <Table.Cell>
class="h-7 w-7 cursor-pointer" <div class="flex gap-1">
> <Tooltip.Provider>
<Trash2Icon class="h-3.5 w-3.5" /> <Tooltip.Root>
</Button> <Tooltip.Trigger>
</Tooltip.Trigger> <Button
<Tooltip.Content> size="icon"
<p>Delete</p> variant="outline"
</Tooltip.Content> class="h-7 w-7 cursor-pointer"
</Tooltip.Root> onclick={() => removeTest(test.id)}
</Tooltip.Provider> >
</div> <Trash2Icon class="h-3.5 w-3.5" />
</Table.Cell> </Button>
</Table.Row> </Tooltip.Trigger>
<Tooltip.Content>
<p>Delete</p>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
</div>
</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body> </Table.Body>
</Table.Root> </Table.Root>
{:else} {:else}

View File

@ -8,11 +8,13 @@
import Clock2Icon from "@lucide/svelte/icons/clock-2"; import Clock2Icon from "@lucide/svelte/icons/clock-2";
const id = $props.id(); const id = $props.id();
let { title, parentFunction, value = $bindable(""), disabled = false } = $props(); let { title, parentFunction, value = $bindable(""), disabled = false, allowFuture = false } = $props();
let open = $state(false); let open = $state(false);
let calendarValue = $state(); let calendarValue = $state();
let timeValue = $state("00:00:00"); let timeValue = $state("00:00:00");
const maxValue = $derived(allowFuture ? undefined : today(getLocalTimeZone()));
$effect(() => { $effect(() => {
if (value && typeof value === "string") { if (value && typeof value === "string") {
try { try {
@ -81,7 +83,7 @@
bind:value={calendarValue} bind:value={calendarValue}
captionLayout="dropdown" captionLayout="dropdown"
onValueChange={handleChange} onValueChange={handleChange}
maxValue={today(getLocalTimeZone())} maxValue={maxValue}
/> />
</div> </div>
<div class="flex flex-col gap-6 border-t p-4"> <div class="flex flex-col gap-6 border-t p-4">

View File

@ -20,6 +20,13 @@
modeOpt: 'default', modeOpt: 'default',
saveEndpoint: createOrder, saveEndpoint: createOrder,
editEndpoint: editOrder, editEndpoint: editOrder,
mapToForm: (data) => ({
...data,
Tests: (data.Tests || []).map((t, i) => ({
...t,
id: t.id ?? i + 1
}))
})
} }
}); });