From 45a6f116ccbabf43b79820f41dcd259f901df6ac Mon Sep 17 00:00:00 2001 From: faiztyanirh Date: Mon, 6 Apr 2026 15:16:23 +0700 Subject: [PATCH] implement selective update patient list --- package-lock.json | 17 --- src/lib/api/api-client.js | 19 --- .../testmap/config/testmap-form-config.js | 20 ++- .../testmap/page/create-page.svelte | 60 ++++---- src/lib/components/order/api/order-api.js | 22 +++ .../components/order/config/order-config.js | 72 +++++++++ .../order/config/order-form-config.js | 22 +++ .../order/modal/search-param-modal.svelte | 142 ++++++++++++++++++ .../components/order/page/create-page.svelte | 0 .../components/order/page/edit-page.svelte | 0 .../components/order/page/master-page.svelte | 90 +++++++++++ .../components/order/page/view-page.svelte | 0 .../components/order/table/order-columns.js | 14 ++ .../admission/config/admission-config.js | 3 +- .../admission/modal/search-param-modal.svelte | 2 +- .../patient/list/config/patient-config.js | 4 +- .../list/config/patient-form-config.js | 2 +- .../patient/list/page/edit-page.svelte | 29 +++- .../reusable/patient-form-renderer.svelte | 2 +- .../components/topbar/topbar-action.svelte | 2 +- src/routes/order/+page.svelte | 55 +++++++ 21 files changed, 503 insertions(+), 74 deletions(-) create mode 100644 src/lib/components/order/api/order-api.js create mode 100644 src/lib/components/order/config/order-config.js create mode 100644 src/lib/components/order/config/order-form-config.js create mode 100644 src/lib/components/order/modal/search-param-modal.svelte create mode 100644 src/lib/components/order/page/create-page.svelte create mode 100644 src/lib/components/order/page/edit-page.svelte create mode 100644 src/lib/components/order/page/master-page.svelte create mode 100644 src/lib/components/order/page/view-page.svelte create mode 100644 src/lib/components/order/table/order-columns.js create mode 100644 src/routes/order/+page.svelte diff --git a/package-lock.json b/package-lock.json index 19d2817..914047f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3724,23 +3724,6 @@ "node": ">=0.10.0" } }, - "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/src/lib/api/api-client.js b/src/lib/api/api-client.js index 63ab0f7..514c5c5 100644 --- a/src/lib/api/api-client.js +++ b/src/lib/api/api-client.js @@ -15,25 +15,6 @@ function cleanQuery(searchQuery) { return result; } -// export async function getById(endpoint, id) { -// try { -// const res = await fetch(`${API.BASE_URL}${endpoint}/${id}`); - -// if (!res.ok) { -// const error = await res.json(); -// console.error('API Error:', error); -// return { data: null, error }; -// } - -// const response = await res.json(); -// console.log(response); -// return { data: response.data?.[0] || response.data, error: null }; -// } catch (err) { -// console.error('Network Error:', err); -// return { data: null, error: err }; -// } -// } - export async function getById(endpoint, id, returnAll = false) { try { const res = await fetch(`${API.BASE_URL}${endpoint}/${id}`); diff --git a/src/lib/components/dictionary/testmap/config/testmap-form-config.js b/src/lib/components/dictionary/testmap/config/testmap-form-config.js index d7d3571..c0bf5a2 100644 --- a/src/lib/components/dictionary/testmap/config/testmap-form-config.js +++ b/src/lib/components/dictionary/testmap/config/testmap-form-config.js @@ -157,6 +157,22 @@ export function getTestMapFormActions(handlers) { ]; } -export function buildTestPayload() { - +export function buildTestMapPayload({ + mainForm, + tempMap, +}) { + const { HostTestCode, HostTestName, ClientTestCode, ClientTestName, ConDefID, ...rest } = mainForm; + + let payload = { + ...rest, + details: tempMap.map((item) => ({ + HostTestCode: item.HostTestCode, + HostTestName: item.HostTestName, + ClientTestCode: item.ClientTestCode, + ClientTestName: item.ClientTestName, + ConDefID: item.ConDefID, + })), + }; + + return cleanEmptyStrings(payload); } \ No newline at end of file diff --git a/src/lib/components/dictionary/testmap/page/create-page.svelte b/src/lib/components/dictionary/testmap/page/create-page.svelte index ac35a60..c3824e4 100644 --- a/src/lib/components/dictionary/testmap/page/create-page.svelte +++ b/src/lib/components/dictionary/testmap/page/create-page.svelte @@ -11,6 +11,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import { untrack } from "svelte"; import { API } from '$lib/config/api'; + import { buildTestMapPayload } from "$lib/components/dictionary/testmap/config/testmap-form-config"; let props = $props(); @@ -37,21 +38,12 @@ function snapshotForm() { return untrack(() => { const f = formState.form; - // const options = {}; - // for (const key in masterDetail.formState.selectOptions) { - // options[key] = [...masterDetail.formState.selectOptions[key]]; - // } return { - HostType: f.HostType ?? "", - HostID: f.HostID ?? "", HostTestCode: f.HostTestCode ?? "", HostTestName: f.HostTestName ?? "", - ClientType: f.ClientType ?? "", - ClientID: f.ClientID ?? "", ClientTestCode: f.ClientTestCode ?? "", ClientTestName: f.ClientTestName ?? "", ConDefID: f.ConDefID ?? "", - // options: options }; }); } @@ -61,6 +53,18 @@ editingId = null; } + function resetTest() { + untrack(() => { + const f = formState.form; + f.HostTestCode = null; + f.HostTestName = null; + f.ClientTestCode = null; + f.ClientTestName = null; + f.ConDefID = null; + }); + editingId = null; + } + function handleInsert() { const row = { id: ++idCounter, @@ -69,7 +73,7 @@ tempMap = [...tempMap, row]; - resetForm(); + resetTest(); } async function handleEdit(row) { @@ -77,13 +81,12 @@ untrack(() => { const f = formState.form; - console.log(row); - f.HostType = row.HostType; - f.HostID = row.HostID; + // f.HostType = row.HostType; + // f.HostID = row.HostID; f.HostTestCode = row.HostTestCode; f.HostTestName = row.HostTestName; - f.ClientType = row.ClientType; - f.ClientID = row.ClientID; + // f.ClientType = row.ClientType; + // f.ClientID = row.ClientID; f.ClientTestCode = row.ClientTestCode; f.ClientTestName = row.ClientTestName; f.ConDefID = row.ConDefID; @@ -100,22 +103,29 @@ tempMap = tempMap.map((row) => row.id === editingId ? { id: row.id, ...snapshotForm() } : row ); - resetForm(); + resetTest(); } function handleCancelEdit() { - resetForm(); + resetTest(); } function handleRemove(id) { tempMap = tempMap.filter((row) => row.id !== id); if (editingId === id) { - resetForm(); + resetTest(); } } async function handleSave() { - const result = await formState.save(masterDetail.mode); + const mainForm = masterDetail.formState.form; + + const payload = buildTestMapPayload({ + mainForm, + tempMap, + }); + console.log(payload) + const result = await formState.save(masterDetail.mode, payload); toast('Test Map Created!'); masterDetail?.exitForm(true); @@ -243,12 +253,12 @@ - Host Type - Host ID + Host Test Code Host Test Name - Client Type - Client ID + Client Test Code Client Test Name Container @@ -267,12 +277,8 @@ - {row.HostType} - {row.HostID} {row.HostTestCode} {row.HostTestName} - {row.ClientType} - {row.ClientID} {row.ClientTestCode} {row.ClientTestName} {row.ConDefID} diff --git a/src/lib/components/order/api/order-api.js b/src/lib/components/order/api/order-api.js new file mode 100644 index 0000000..1384cb7 --- /dev/null +++ b/src/lib/components/order/api/order-api.js @@ -0,0 +1,22 @@ +import { API } from '$lib/config/api.js'; +import { getById, searchWithParams, create, update } from '$lib/api/api-client'; + +export async function searchParam(searchQuery) { + return await searchWithParams(API.PATIENTS, searchQuery) +} + +export async function getVisitList(searchQuery) { + return await getById(API.VISITLIST, searchQuery, true) +} + +export async function getVisit(searchQuery) { + return await getById(API.PATVISIT, searchQuery) +} + +export async function createOrder(newOrderForm) { + return await create(API.ORDER, newOrderForm) +} + +export async function editOrder(editOrderForm) { + return await update(API.ORDER, editOrderForm) +} \ No newline at end of file diff --git a/src/lib/components/order/config/order-config.js b/src/lib/components/order/config/order-config.js new file mode 100644 index 0000000..a699b5f --- /dev/null +++ b/src/lib/components/order/config/order-config.js @@ -0,0 +1,72 @@ +import PlusIcon from "@lucide/svelte/icons/plus"; +import Settings2Icon from "@lucide/svelte/icons/settings-2"; +import PencilIcon from "@lucide/svelte/icons/pencil"; + +export const searchFields = [ + { + key: "PatientID", + label: "Patient ID", + placeholder: "", + type: "text", + defaultValue: "", + }, + { + key: "Name", + label: "Patient Name", + placeholder: "", + type: "text", + defaultValue: "", + }, + { + key: "Birthdate", + label: "Birthdate", + type: "date" + }, + { + key: "Identifier", + label: "Identifier", + type: "text" + }, + { + key: "VisitID", + label: "Visit ID", + type: "text" + }, + { + key: "EpisodeID", + label: "Episode ID", + type: "text" + }, +]; + +export const detailSections = []; + +export function orderActions(masterDetail, selectedPatient) { + return [ + { + Icon: PlusIcon, + label: 'Add Order', + onClick: () => masterDetail.enterCreate({ + PatientID: selectedPatient?.PatientID, + InternalPID: selectedPatient?.InternalPID + }), + disabled: !selectedPatient, + }, + { + Icon: Settings2Icon, + label: 'Search Parameters', + popoverWidth: "w-full", + collisionPadding: 12, + }, + ]; +} + +export function viewActions(handlers){ + return [ + { + Icon: PencilIcon, + label: 'Edit Order', + onClick: handlers.editOrder, + }, + ] +} \ No newline at end of file diff --git a/src/lib/components/order/config/order-form-config.js b/src/lib/components/order/config/order-form-config.js new file mode 100644 index 0000000..288bb14 --- /dev/null +++ b/src/lib/components/order/config/order-form-config.js @@ -0,0 +1,22 @@ +import { API } from "$lib/config/api"; +import EraserIcon from "@lucide/svelte/icons/eraser"; +import { z } from "zod"; +import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings"; + +export const orderSchema = z.object({}); + +export const orderInitialForm = {}; + +export const orderDefaultErrors = {}; + +export const orderFormFields = []; + +export function getOrderFormActions(handlers) { + return [ + { + Icon: EraserIcon, + label: 'Clear Form', + onClick: handlers.clearForm, + }, + ]; +} \ No newline at end of file diff --git a/src/lib/components/order/modal/search-param-modal.svelte b/src/lib/components/order/modal/search-param-modal.svelte new file mode 100644 index 0000000..3bcd3d0 --- /dev/null +++ b/src/lib/components/order/modal/search-param-modal.svelte @@ -0,0 +1,142 @@ + + +
+
+
+
+ {#each props.searchFields as field} + {#if field.type === "text"} +
+ + +
+ {:else if field.type === "date"} +
+ +
+ {:else if field.type === "select"} +
+ + + + + + + {#each field.options as opt} + + {opt.label} + + {/each} + + +
+ {/if} + {/each} +
+
+ + +
+
+
+ {#if props.search.searchData && props.search.searchData.length > 0} +
+
+ + + + + Patient ID + Patient Name + Birthdate + Sex + + + + {#each props.search.searchData as patient, i} + handleCheckboxChange(patient)} + > + e.stopPropagation()}> + handleCheckboxChange(patient)} + /> + + {patient.PatientID} + {patient.FullName} + {patient.Birthdate ? patient.Birthdate.split(" ")[0] : ""} + {patient.SexLabel} + + {/each} + + +
+
+
+ + + +
+ {:else} +
+ +
+ {/if} +
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/src/lib/components/order/page/create-page.svelte b/src/lib/components/order/page/create-page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/components/order/page/edit-page.svelte b/src/lib/components/order/page/edit-page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/components/order/page/master-page.svelte b/src/lib/components/order/page/master-page.svelte new file mode 100644 index 0000000..13861ef --- /dev/null +++ b/src/lib/components/order/page/master-page.svelte @@ -0,0 +1,90 @@ + + +{#snippet searchParamSnippet()} + +{/snippet} + +
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%]"} + transition-all duration-300 flex flex-col items-center p-2 h-full overflow-y-auto + `} +> +
+ {#if props.masterDetail.isFormMode} + + {#each "ADMISSION".split("") as c} + {c} + {/each} + + {/if} + + {#if !props.masterDetail.isFormMode} +
e.stopPropagation()} onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + } + }}> + +
+ {#if searchData?.data?.length > 0} + + {:else} +
+ +
+ {/if} +
+
+ {/if} +
+
\ No newline at end of file diff --git a/src/lib/components/order/page/view-page.svelte b/src/lib/components/order/page/view-page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/components/order/table/order-columns.js b/src/lib/components/order/table/order-columns.js new file mode 100644 index 0000000..8ff94d4 --- /dev/null +++ b/src/lib/components/order/table/order-columns.js @@ -0,0 +1,14 @@ +export const orderColumns = [ + { + accessorKey: "InternalPVID", + header: "Patient ID" + }, + { + accessorKey: "PVID", + header: "Visit ID", + }, + { + accessorKey: "EpisodeID", + header: "Episode ID", + }, +]; \ No newline at end of file diff --git a/src/lib/components/patient/admission/config/admission-config.js b/src/lib/components/patient/admission/config/admission-config.js index 4153e94..7936fa5 100644 --- a/src/lib/components/patient/admission/config/admission-config.js +++ b/src/lib/components/patient/admission/config/admission-config.js @@ -93,7 +93,8 @@ export function admissionActions(masterDetail, selectedPatient) { { Icon: Settings2Icon, label: 'Search Parameters', - popoverWidth: "w-256", + popoverWidth: "w-full", + collisionPadding: 12, }, ]; } diff --git a/src/lib/components/patient/admission/modal/search-param-modal.svelte b/src/lib/components/patient/admission/modal/search-param-modal.svelte index 3781e36..42d5168 100644 --- a/src/lib/components/patient/admission/modal/search-param-modal.svelte +++ b/src/lib/components/patient/admission/modal/search-param-modal.svelte @@ -46,7 +46,7 @@
-
+
{#each props.searchFields as field} {#if field.type === "text"}
diff --git a/src/lib/components/patient/list/config/patient-config.js b/src/lib/components/patient/list/config/patient-config.js index 69bc792..56b047f 100644 --- a/src/lib/components/patient/list/config/patient-config.js +++ b/src/lib/components/patient/list/config/patient-config.js @@ -76,10 +76,10 @@ export const detailSections = [ { parentKey: "Custodian", key: "PatientID", label: "Custodian ID" }, ], }, - { key: "DeathIndicatorLabel", label: "Death Indicator" }, + { key: "isDeadLabel", label: "Deceased" }, { key: "CreateDate", label: "Create Date", isUTCDate: true }, { key: "DelDate", label: "Disabled Date" }, - { key: "TimeOfDeath", label: "Death Date", isUTCDate: true }, + { key: "TimeOfDeath", label: "Time of Death", isUTCDate: true }, ] }, // { diff --git a/src/lib/components/patient/list/config/patient-form-config.js b/src/lib/components/patient/list/config/patient-form-config.js index 07462bb..9a09fd6 100644 --- a/src/lib/components/patient/list/config/patient-form-config.js +++ b/src/lib/components/patient/list/config/patient-form-config.js @@ -270,7 +270,7 @@ export const patientFormFields = [ type: "group", columns: [ { - key: "DeathIndicator", + key: "isDead", label: "Deceased", required: false, type: "select", diff --git a/src/lib/components/patient/list/page/edit-page.svelte b/src/lib/components/patient/list/page/edit-page.svelte index c80db35..da446b3 100644 --- a/src/lib/components/patient/list/page/edit-page.svelte +++ b/src/lib/components/patient/list/page/edit-page.svelte @@ -47,7 +47,6 @@ }); }); - // if (formState.form.ProvinceID && formState.form.CityID) { if (formState.form.Province) { formState.fetchOptions( { @@ -61,9 +60,35 @@ } }); }); + + function getChangedFields(original, current) { + const changed = {}; + for (const key in current) { + if (JSON.stringify(current[key]) !== JSON.stringify(original[key])) { + changed[key] = current[key]; + } + } + return changed; + } async function handleEdit() { - const payload = buildPatientPayload(formState.form); + const currentPayload = buildPatientPayload(formState.form); + const originalPayload = buildPatientPayload(masterDetail.formSnapshot); + + const changedFields = getChangedFields(originalPayload, currentPayload); + + if (Object.keys(changedFields).length === 0) { + toast('No changes detected'); + return; + } + + const payload = { + InternalPID: formState.form.InternalPID, + ...changedFields + }; + + console.log('Payload:', payload); + const result = await formState.save(masterDetail.mode, payload); if (result.status === 'success') { diff --git a/src/lib/components/patient/reusable/patient-form-renderer.svelte b/src/lib/components/patient/reusable/patient-form-renderer.svelte index 2be932a..10b514e 100644 --- a/src/lib/components/patient/reusable/patient-form-renderer.svelte +++ b/src/lib/components/patient/reusable/patient-form-renderer.svelte @@ -68,7 +68,7 @@ } let isDeathDateDisabled = $derived( - formState.form.DeathIndicator !== 'Y' + formState.form.isDead !== 'Y' ); $effect(() => { diff --git a/src/lib/components/topbar/topbar-action.svelte b/src/lib/components/topbar/topbar-action.svelte index e5b0c42..37bee19 100644 --- a/src/lib/components/topbar/topbar-action.svelte +++ b/src/lib/components/topbar/topbar-action.svelte @@ -27,7 +27,7 @@ {/snippet} - + {@render props.popoverContent()} diff --git a/src/routes/order/+page.svelte b/src/routes/order/+page.svelte new file mode 100644 index 0000000..0579089 --- /dev/null +++ b/src/routes/order/+page.svelte @@ -0,0 +1,55 @@ + + +
+ {#if masterDetail.showMaster} + + {/if} + + {#if masterDetail.showDetail} +
+ {#if masterDetail.mode === "view"} + + {:else if masterDetail.mode === "create"} + + {:else if masterDetail.mode === "edit"} + + {/if} +
+ {/if} +
\ No newline at end of file