From f8af8c21e69221f3640a6f0918c414dd5592de16 Mon Sep 17 00:00:00 2001 From: faiztyanirh Date: Wed, 22 Apr 2026 17:18:56 +0700 Subject: [PATCH] continue ordertest create & edit --- src/lib/api/api-client.js | 1 - .../composable/use-order-form.svelte.js | 12 ++ .../dictionary/testmap/page/edit-page.svelte | 1 - .../ordertest/config/ordertest-config.js | 10 +- .../ordertest/config/ordertest-form-config.js | 53 ++++++- .../order/ordertest/page/create-page.svelte | 34 ++-- .../order/ordertest/page/edit-page.svelte | 69 ++++++++ .../order/ordertest/page/master-page.svelte | 6 +- .../order/ordertest/page/view-page.svelte | 74 ++++++++- .../ordertest/table/ordertest-columns.js | 12 ++ .../reusable/form-page-container.svelte | 1 - .../reusable/form/order-form-renderer.svelte | 149 ++++++++++++------ .../reusable-calendar-timepicker.svelte | 6 +- src/routes/order/ordertest/+page.svelte | 7 + 14 files changed, 341 insertions(+), 94 deletions(-) create mode 100644 src/lib/components/composable/use-order-form.svelte.js diff --git a/src/lib/api/api-client.js b/src/lib/api/api-client.js index c584421..bde2bb0 100644 --- a/src/lib/api/api-client.js +++ b/src/lib/api/api-client.js @@ -46,7 +46,6 @@ export async function searchWithParams(endpoint, searchQuery) { : `${API.BASE_URL}${endpoint}`; const res = await fetch(url); const data = await res.json(); - console.log(url); return data.data || []; } catch (err) { console.error('Search Error:', err); diff --git a/src/lib/components/composable/use-order-form.svelte.js b/src/lib/components/composable/use-order-form.svelte.js new file mode 100644 index 0000000..7b6a41e --- /dev/null +++ b/src/lib/components/composable/use-order-form.svelte.js @@ -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 }, + } +} \ No newline at end of file diff --git a/src/lib/components/dictionary/testmap/page/edit-page.svelte b/src/lib/components/dictionary/testmap/page/edit-page.svelte index 5c11df7..8f75eb2 100644 --- a/src/lib/components/dictionary/testmap/page/edit-page.svelte +++ b/src/lib/components/dictionary/testmap/page/edit-page.svelte @@ -385,7 +385,6 @@ reset: () => { tempMap = []; idCounter = 0; editingId = null; }, }); }); - $inspect(testMapDetailFormState.form) diff --git a/src/lib/components/order/ordertest/config/ordertest-config.js b/src/lib/components/order/ordertest/config/ordertest-config.js index c1f6db0..aa1f976 100644 --- a/src/lib/components/order/ordertest/config/ordertest-config.js +++ b/src/lib/components/order/ordertest/config/ordertest-config.js @@ -43,8 +43,12 @@ export const detailSections = [ { class: "grid grid-cols-2 gap-4 items-center", fields: [ - { key: "OrderID", label: "Order 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', onClick: () => masterDetail.enterCreate({ PatientID: selectedPatient?.PatientID, - InternalPID: selectedPatient?.InternalPID + PatientName: selectedPatient?.FullName, + InternalPID: selectedPatient?.InternalPID, + PVADTID: selectedVisit?.PVADTID, }), disabled: !selectedPatient, }, diff --git a/src/lib/components/order/ordertest/config/ordertest-form-config.js b/src/lib/components/order/ordertest/config/ordertest-form-config.js index 1f6c2cd..c49211c 100644 --- a/src/lib/components/order/ordertest/config/ordertest-form-config.js +++ b/src/lib/components/order/ordertest/config/ordertest-form-config.js @@ -3,7 +3,32 @@ import EraserIcon from "@lucide/svelte/icons/eraser"; import { z } from "zod"; 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 = { InternalOID: '', @@ -18,10 +43,12 @@ export const orderTestInitialForm = { EffDate: '', Comment: '', OrderAtt: '', - Tests: '', + Tests: [], }; -export const orderTestDefaultErrors = {}; +export const orderTestDefaultErrors = { + Tests: '', +}; export const orderTestFormFields = [ { @@ -72,14 +99,14 @@ export const orderTestFormFields = [ key: "TrnDate", label: "Transaction Date", required: false, - type: "date", + type: "datetime", allowFuture: true }, { key: "EffDate", label: "Effective Date", required: false, - type: "date", + type: "datetime", allowFuture: true }, ] @@ -117,11 +144,12 @@ export const orderTestFormFields = [ { key: "Tests", label: "Search Test", - required: false, + required: true, type: "tests", optionsEndpoint: `${API.BASE_URL}${API.TEST}`, valueKey: 'TestSiteCode', labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`, + validateOn: ['input'] }, ] }, @@ -137,4 +165,17 @@ export function getOrderTestFormActions(handlers) { onClick: handlers.clearForm, }, ]; +} + +export function buildOrderTestPayload(mainForm){ + const { InternalOID, PatientID, PatientName, Tests, ...rest } = mainForm; + + let payload = { + ...rest, + // Tests: Tests.map(test => ({ + // TestSiteID: test.testSiteID, + // })) + } + + return cleanEmptyStrings(payload) } \ No newline at end of file diff --git a/src/lib/components/order/ordertest/page/create-page.svelte b/src/lib/components/order/ordertest/page/create-page.svelte index fdedf93..cf23fea 100644 --- a/src/lib/components/order/ordertest/page/create-page.svelte +++ b/src/lib/components/order/ordertest/page/create-page.svelte @@ -1,9 +1,10 @@ - + + 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) + + + + + + + \ No newline at end of file diff --git a/src/lib/components/order/ordertest/page/master-page.svelte b/src/lib/components/order/ordertest/page/master-page.svelte index d6923b1..e7b7365 100644 --- a/src/lib/components/order/ordertest/page/master-page.svelte +++ b/src/lib/components/order/ordertest/page/master-page.svelte @@ -22,7 +22,7 @@ let selectedVisit = $state(null); let isLoading = $state(false); let searchData = $state([]); -$inspect(selectedPatient) + const search = useSearch(searchFields, searchParam); let actions = $derived.by(() => { @@ -85,7 +85,7 @@ $inspect(selectedPatient) onclick={() => props.masterDetail.isFormMode && props.masterDetail.exitForm()} onkeydown={(e) => e.key === 'Enter' && props.masterDetail.isFormMode && props.masterDetail.exitForm()} 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 `} > @@ -136,7 +136,7 @@ $inspect(selectedPatient) {/if}
{#if searchData?.length > 0} - + {:else}
diff --git a/src/lib/components/order/ordertest/page/view-page.svelte b/src/lib/components/order/ordertest/page/view-page.svelte index 08c8077..b65a97e 100644 --- a/src/lib/components/order/ordertest/page/view-page.svelte +++ b/src/lib/components/order/ordertest/page/view-page.svelte @@ -3,6 +3,8 @@ import { detailSections, viewActions } from "$lib/components/order/ordertest/config/ordertest-config"; import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.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(); @@ -28,8 +30,45 @@ return field.parentKey ? order[field.parentKey]?.[field.key] : order[field.key]; } + +{#snippet DetailsTable({ value, label })} +
+
+ {label} +
+
+ {#if value && Array.isArray(value) && value.length > 0} +
+ + + + Test Code + Test Name + Discipline + Create Date + + + + {#each value as row, i} + + {row.TestSiteCode ?? '-'} + {row.TestSiteName ?? '-'} + {row.Discipline.DisciplineName ?? '-'} + {formatUTCDate(row.CreateDate) ?? '-'} + + {/each} + + +
+ {:else} + - + {/if} +
+
+{/snippet} + {#snippet Fieldset({ value, label, isUTCDate = false })}
@@ -45,7 +84,11 @@
{/snippet} -{#if masterDetail.selectedItem} +{#if masterDetail.isLoadingDetail} +
+ +
+{:else if masterDetail.selectedItem}
{#each detailSections as section} -
-
+
+
{#each section.fields as field} - {@render Fieldset({ - label: field.label, - value: getFieldValue(field), - isUTCDate: field.isUTCDate - })} + {#if field.fullWidth} +
+ {#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} +
+ {: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}
diff --git a/src/lib/components/order/ordertest/table/ordertest-columns.js b/src/lib/components/order/ordertest/table/ordertest-columns.js index d205cf3..e7bc9ff 100644 --- a/src/lib/components/order/ordertest/table/ordertest-columns.js +++ b/src/lib/components/order/ordertest/table/ordertest-columns.js @@ -7,4 +7,16 @@ export const orderTestColumns = [ accessorKey: "PlacerID", header: "Host ID", }, + { + accessorKey: "TrnDate", + header: "Transaction Date", + }, + { + accessorKey: "EffDate", + header: "Effective Date", + }, + { + accessorKey: "Priority", + header: "Priority", + }, ]; \ No newline at end of file diff --git a/src/lib/components/patient/reusable/form-page-container.svelte b/src/lib/components/patient/reusable/form-page-container.svelte index 07f35e4..8b71a7f 100644 --- a/src/lib/components/patient/reusable/form-page-container.svelte +++ b/src/lib/components/patient/reusable/form-page-container.svelte @@ -20,7 +20,6 @@
{@render children()}
-
{/if}
-
{#if type === 'text'} { formState.form[key] = val; + if (validateOn?.includes('input')) { + formState.validateField?.(key, formState.form[key], false); + } }} onOpenChange={(open) => { if (open) { @@ -184,30 +216,31 @@ {/if} - {:else if type === 'date'} - { - formState.form[key] = dateStr; - if (validateOn?.includes('input')) { - formState.validateField(key, dateStr, false); - } - }} + {:else if type === "datetime"} + { + formState.form[key] = val; + if (validateOn?.includes("input")) { + formState.validateField(key, val, false); + } + }} allowFuture={allowFuture} - /> + /> {:else if type === "fileupload"}
{:else if type === "tests"} - {@const selectedLabel = - formState.selectOptions?.[key]?.find((opt) => opt.value === formState.form[key])?.label || - 'Choose'} + {@const selectedLabel = selectedTest?.label || 'Choose'} {@const filteredOptions = getFilteredOptions(key)}
{ + selectedTest = filteredOptions.find((opt) => opt.value === val) ?? null; + }} onOpenChange={(open) => { if (open && optionsEndpoint) { formState.fetchOptions?.( @@ -236,58 +269,70 @@ - None - {/if} {#each filteredOptions as option} - - {option.label} - + + {option.label} + {/each} {/if}
-
- + - No + No Test Name - Discipline - Container + Test Code - - 1 - Hematologi Rutin - Hematologi - EDTA - -
- - - - - - -

Delete

-
-
-
-
-
-
+ {#if formState.form.Tests.length === 0} + + + No data. Add a test from Search Test above. + + + {:else} + {#each formState.form.Tests as test, index (test.id)} + + {index + 1} + {test.TestSiteName} + {test.TestSiteCode} + +
+ + + + + + +

Delete

+
+
+
+
+
+
+ {/each} + {/if}
{:else} diff --git a/src/lib/components/reusable/reusable-calendar-timepicker.svelte b/src/lib/components/reusable/reusable-calendar-timepicker.svelte index b22f96d..e72bcaf 100644 --- a/src/lib/components/reusable/reusable-calendar-timepicker.svelte +++ b/src/lib/components/reusable/reusable-calendar-timepicker.svelte @@ -8,11 +8,13 @@ import Clock2Icon from "@lucide/svelte/icons/clock-2"; 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 calendarValue = $state(); let timeValue = $state("00:00:00"); + const maxValue = $derived(allowFuture ? undefined : today(getLocalTimeZone())); + $effect(() => { if (value && typeof value === "string") { try { @@ -81,7 +83,7 @@ bind:value={calendarValue} captionLayout="dropdown" onValueChange={handleChange} - maxValue={today(getLocalTimeZone())} + maxValue={maxValue} />
diff --git a/src/routes/order/ordertest/+page.svelte b/src/routes/order/ordertest/+page.svelte index 956e45d..f41946f 100644 --- a/src/routes/order/ordertest/+page.svelte +++ b/src/routes/order/ordertest/+page.svelte @@ -20,6 +20,13 @@ modeOpt: 'default', saveEndpoint: createOrder, editEndpoint: editOrder, + mapToForm: (data) => ({ + ...data, + Tests: (data.Tests || []).map((t, i) => ({ + ...t, + id: t.id ?? i + 1 + })) + }) } });