From aee9ea4b8fff48bdf082172180196340a4a64836 Mon Sep 17 00:00:00 2001 From: faiztyanirh Date: Mon, 9 Feb 2026 17:31:02 +0700 Subject: [PATCH] initial create admission, fix view detail admission --- .../composable/use-form-option.svelte.js | 22 +- .../admission/api/patient-admission-api.js | 15 +- .../admission/config/admission-config.js | 72 ++++++ .../admission/config/admission-form-config.js | 209 ++++++++++++++++++ .../patient/admission/page/create-page.svelte | 61 ++++- .../patient/admission/page/view-page.svelte | 113 +++++++++- .../patient/list/page/view-page.svelte | 1 - .../reusable/patient-form-renderer.svelte | 27 ++- src/lib/components/ui/toggle-group/index.js | 10 + .../ui/toggle-group/toggle-group-item.svelte | 35 +++ .../ui/toggle-group/toggle-group.svelte | 52 +++++ src/lib/components/ui/toggle/index.js | 11 + src/lib/components/ui/toggle/toggle.svelte | 46 ++++ src/routes/patient/admission/+page.svelte | 1 - 14 files changed, 656 insertions(+), 19 deletions(-) create mode 100644 src/lib/components/ui/toggle-group/index.js create mode 100644 src/lib/components/ui/toggle-group/toggle-group-item.svelte create mode 100644 src/lib/components/ui/toggle-group/toggle-group.svelte create mode 100644 src/lib/components/ui/toggle/index.js create mode 100644 src/lib/components/ui/toggle/toggle.svelte diff --git a/src/lib/components/composable/use-form-option.svelte.js b/src/lib/components/composable/use-form-option.svelte.js index 67deb3c..603169e 100644 --- a/src/lib/components/composable/use-form-option.svelte.js +++ b/src/lib/components/composable/use-form-option.svelte.js @@ -8,8 +8,17 @@ const optionsMode = { const res = await fetch(field.optionsEndpoint); const json = await res.json(); - selectOptions[field.key] = json?.data ?? []; + // selectOptions[field.key] = json?.data ?? []; + const data = json?.data ?? []; + const valueKey = field.valueKey ?? 'value'; + const labelKey = field.labelKey ?? 'label'; + + selectOptions[field.key] = data.map((item) => ({ + value: item[valueKey], + label: typeof labelKey === 'function' ? labelKey(item) : item[labelKey], + })); +console.log(selectOptions); } catch (err) { console.error("Failed to fetch options for", field.key, err); selectOptions[field.key] = []; @@ -52,8 +61,17 @@ const optionsMode = { const res = await fetch(endpoint); const json = await res.json(); - selectOptions[field.key] = json?.data ?? []; + // selectOptions[field.key] = json?.data ?? []; + const data = json?.data ?? []; + const valueKey = field.valueKey ?? 'value'; + const labelKey = field.labelKey ?? 'label'; + + selectOptions[field.key] = data.map((item) => ({ + value: item[valueKey], + label: typeof labelKey === 'function' ? labelKey(item) : item[labelKey], + })); +console.log(selectOptions); // Track last fetched parent value for dependent fields if (field.dependsOn) { lastFetched[field.key] = parentValue; diff --git a/src/lib/components/patient/admission/api/patient-admission-api.js b/src/lib/components/patient/admission/api/patient-admission-api.js index 8d612bf..8b2d0ed 100644 --- a/src/lib/components/patient/admission/api/patient-admission-api.js +++ b/src/lib/components/patient/admission/api/patient-admission-api.js @@ -13,17 +13,10 @@ export async function getVisit(searchQuery) { return await getById(API.PATVISIT, searchQuery) } -export async function getPatient(searchQuery) { - const { data: patient, error } = await getById(API.PATIENTS, searchQuery) - return { patient }; +export async function createAdmission(newAdmissionForm) { + return await create(API.PATVISIT, newAdmissionForm) } -export async function createPatient(newContactForm) { - // console.log(JSON.stringify(newContactForm)); - return await create(API.PATIENTS, newContactForm) -} - -export async function editPatient(editContactForm) { - // console.log(JSON.stringify(editContactForm)); - return await update(API.PATIENTS, editContactForm) +export async function editAdmission(editAdmissionForm) { + return await update(API.PATVISIT, editAdmissionForm) } \ 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 c4b8229..70275fd 100644 --- a/src/lib/components/patient/admission/config/admission-config.js +++ b/src/lib/components/patient/admission/config/admission-config.js @@ -1,5 +1,6 @@ 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 = [ { @@ -38,6 +39,66 @@ export const searchFields = [ }, ]; +// export const detailSections = [ +// { +// class: "grid grid-cols-1 sm:grid-cols-2 gap-3", +// fields: [ +// { key: "PVID", label: "Visit ID" }, +// { key: "EpisodeID", label: "Episode ID" }, +// { key: "", label: "Visit Class" }, +// { key: "", label: "Service Class" }, +// { key: "LocationID", label: "Location" }, +// { key: "AttDoc", label: "Attending Doctor" }, +// { key: "RefDoc", label: "Reffering Doctor" }, +// { key: "AdmDoc", label: "Admitting Doctor" }, +// { key: "CnsDoc", label: "Consulting Doctor" }, +// { key: "", label: "Admission Date", isUTCDate: true }, +// { key: "", label: "Discharge Date", isUTCDate: true }, +// { key: "Diagnosis", label: "Clinical Diagnosis" }, +// ] +// }, +// ] + +export const detailSections = [ + { + title: "", // No title for top row + class: "grid grid-cols-1 md:grid-cols-2 gap-4", + groups: [ + { + class: "space-y-3", + fields: [ + { key: "PVID", label: "Visit ID" }, + { key: "EpisodeID", label: "Episode ID" }, + { key: "VisitClass", label: "Visit Class" }, + { key: "ServiceClass", label: "Service Class" }, + ] + }, + { + class: "space-y-3", + fields: [ + { key: "LocationID", label: "Location" }, + { key: "AttDoc", label: "Attending Doctor" }, + { key: "RefDoc", label: "Referring Doctor" }, + { key: "AdmDoc", label: "Admitting Doctor" }, + { key: "CnsDoc", label: "Consulting Doctor" }, + ] + } + ] + }, + { + class: "grid grid-cols-2 gap-4 items-center", + fields: [ + { key: "AdmissionDate", label: "Admission Date", isUTCDate: true }, + { key: "DischargeDate", label: "Discharge Date", isUTCDate: true }, + ] + }, + { + fields: [ + { key: "Diagnosis", label: "Clinical Diagnosis" }, + ] + } +]; + export function admissionActions(masterDetail) { return [ { @@ -51,4 +112,15 @@ export function admissionActions(masterDetail) { popoverWidth: "w-256", }, ]; +} + +export function viewActions(handlers){ + return [ + { + Icon: PencilIcon, + label: 'Edit Patient', + onClick: handlers.editPatient, + + }, + ] } \ No newline at end of file diff --git a/src/lib/components/patient/admission/config/admission-form-config.js b/src/lib/components/patient/admission/config/admission-form-config.js index e69de29..0bfd68e 100644 --- a/src/lib/components/patient/admission/config/admission-form-config.js +++ b/src/lib/components/patient/admission/config/admission-form-config.js @@ -0,0 +1,209 @@ +import { API } from "$lib/config/api"; +import EraserIcon from "@lucide/svelte/icons/eraser"; +import { z } from "zod"; + +export const admissionSchema = z.object({}); + +export const admissionInitialForm = { + InternalPVID: "", + InternalPID: "", + PVID: "", + EpisodeID: "", + DiagCode: "", + Diagnosis: "", + ADTCode: "", + LocationID: "", + AttDoc: "", + RefDoc: "", + AdmDoc: "", + CnsDoc: "", +}; + +export const admissionDefaultErrors = {}; + +export const admissionFormFields = [ + { + title: "Visit Information", + rows: [ + { + type: "row", + columns: [ + { + key: "PVID", + label: "Visit ID", + required: false, + type: "text", + }, + { + key: "EpisodeID", + label: "Episode ID", + required: false, + type: "text", + } + ] + } + ] + }, + { + title: "Medical Team", + rows: [ + { + type: "row", + columns: [ + { + key: "AttDoc", + label: "Attended Doctor", + required: false, + type: "select", + optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`, + valueKey: "ContactID", + labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`, + }, + { + key: "RefDoc", + label: "Reference Doctor", + required: false, + type: "select", + optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`, + valueKey: "ContactID", + labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`, + } + ] + }, + { + type: "row", + columns: [ + { + key: "AdmDoc", + label: "Admitted Doctor", + required: false, + type: "select", + optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`, + valueKey: "ContactID", + labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`, + }, + { + key: "CnsDoc", + label: "Consulte Doctor", + required: false, + type: "select", + optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`, + valueKey: "ContactID", + labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`, + } + ] + } + ] + }, + { + title: "Visit Classification", + rows: [ + { + type: "row", + columns: [ + { + key: "LocationID", + label: "Location", + required: false, + type: "select", + optionsEndpoint: `${API.BASE_URL}${API.LOCATION}`, + valueKey: "LocationID", + labelKey: (item) => `${item.LocCode} - ${item.LocFull}`, + }, + { + key: "VisitClass", + label: "Visit Class", + required: false, + type: "select", + optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/visit_classes`, + } + ] + }, + { + type: "row", + columns: [ + { + key: "ServiceClass", + label: "Service Class", + required: false, + type: "select", + optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/service_classes`, + }, + { + key: "Discharge", + label: "Discharge Status", + required: false, + type: "toggle", + defaultValue: false, + } + ] + } + ] + }, + { + title: "Clinical Information", + rows: [ + { + type: "row", + columns: [ + { + key: "Diagnosis", + label: "Diagnosis", + required: false, + type: "textarea", + rows: 4, + maxLength: 1000, + } + ] + } + ] + } +]; + +export function getAdmissionFormActions(handlers) { + return [ + { + Icon: EraserIcon, + label: 'Clear Form', + onClick: handlers.clearForm, + }, + ]; +} + +const admissionTemplate = { + PVID: 'PVID', + InternalPID: 'InternalPID', + EpisodeID: 'EpisodeID', + PatDiag: { + DiagCode: 'DiagCode', + Diagnosis: 'Diagnosis', + }, + PatVisitADT: { + ADTCode: () => 'A04', + LocationID: 'LocationID', + AttDoc: 'AttDoc', + RefDoc: 'RefDoc', + AdmDoc: 'AdmDoc', + CnsDoc: 'CnsDoc', + }, +}; + +export function buildPayload(form, schema = admissionTemplate) { + const payload = {}; + + for (const [key, config] of Object.entries(schema)) { + if (typeof config === 'string') { + // Kirim nilai dari form, atau null jika tidak ada (agar key tetap ada) + payload[key] = form[config] ?? null; + } + else if (typeof config === 'function') { + payload[key] = config(form); + } + else if (typeof config === 'object' && config !== null) { + // Rekursif tanpa pengecekan panjang keys, agar objek nested selalu dibuat + payload[key] = buildPayload(form, config); + } + } + + return payload; +} \ No newline at end of file diff --git a/src/lib/components/patient/admission/page/create-page.svelte b/src/lib/components/patient/admission/page/create-page.svelte index 4649907..a1d7b32 100644 --- a/src/lib/components/patient/admission/page/create-page.svelte +++ b/src/lib/components/patient/admission/page/create-page.svelte @@ -1 +1,60 @@ -cr \ No newline at end of file + + + + + \ No newline at end of file diff --git a/src/lib/components/patient/admission/page/view-page.svelte b/src/lib/components/patient/admission/page/view-page.svelte index 3e0b9eb..fc0c2cc 100644 --- a/src/lib/components/patient/admission/page/view-page.svelte +++ b/src/lib/components/patient/admission/page/view-page.svelte @@ -1 +1,112 @@ -vw \ No newline at end of file + + +{#snippet Fieldset({ value, label, isUTCDate = false })} +
+
+ {label} +
+
+ {#if isUTCDate} + {formatUTCDate(value)} + {:else} + {value ?? "-"} + {/if} +
+
+{/snippet} + +{#if props.masterDetail.selectedItem} +
+ +
+ {#each detailSections as section} +
+ {#if section.groups} +
+ {#each section.groups as group} +
+
+ {#each group.fields as field} + {@render Fieldset({ + label: field.label, + value: getFieldValue(field), + isUTCDate: field.isUTCDate + })} + {/each} +
+
+ {/each} +
+ {:else} +
+ {#each section.fields as field} + {@render Fieldset({ + label: field.label, + value: getFieldValue(field), + isUTCDate: field.isUTCDate + })} + {/each} +
+ {/if} +
+ {/each} +
+
+{:else} + +{/if} + + \ No newline at end of file diff --git a/src/lib/components/patient/list/page/view-page.svelte b/src/lib/components/patient/list/page/view-page.svelte index def7f7a..1834830 100644 --- a/src/lib/components/patient/list/page/view-page.svelte +++ b/src/lib/components/patient/list/page/view-page.svelte @@ -6,7 +6,6 @@ import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte"; let props = $props(); - let patient = $derived(props.masterDetail?.selectedItem?.patient); const handlers = { diff --git a/src/lib/components/patient/reusable/patient-form-renderer.svelte b/src/lib/components/patient/reusable/patient-form-renderer.svelte index e339b8f..87c0eb3 100644 --- a/src/lib/components/patient/reusable/patient-form-renderer.svelte +++ b/src/lib/components/patient/reusable/patient-form-renderer.svelte @@ -1,6 +1,7 @@ -{#snippet Fieldset({ key, label, required, type, optionsEndpoint, options, validateOn, dependsOn, endpointParamKey })} +{#snippet Fieldset({ key, label, required, type, optionsEndpoint, options, validateOn, dependsOn, endpointParamKey, valueKey, labelKey })}
@@ -184,7 +187,7 @@ onOpenChange={(open) => { if (open && optionsEndpoint) { formState.fetchOptions( - { key, optionsEndpoint, dependsOn, endpointParamKey }, + { key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey }, formState.form ); } @@ -279,6 +282,26 @@
{/if}
+ {:else if type === "toggle"} +
+ + + + + + + + + +
{:else} + import { ToggleGroup as ToggleGroupPrimitive } from "bits-ui"; + import { getToggleGroupCtx } from "./toggle-group.svelte"; + import { cn } from "$lib/utils.js"; + import { toggleVariants } from "$lib/components/ui/toggle/index.js"; + + let { + ref = $bindable(null), + value = $bindable(), + class: className, + size, + variant, + ...restProps + } = $props(); + + const ctx = getToggleGroupCtx(); + + + \ No newline at end of file diff --git a/src/lib/components/ui/toggle-group/toggle-group.svelte b/src/lib/components/ui/toggle-group/toggle-group.svelte new file mode 100644 index 0000000..cd9de8f --- /dev/null +++ b/src/lib/components/ui/toggle-group/toggle-group.svelte @@ -0,0 +1,52 @@ + + + + + + \ No newline at end of file diff --git a/src/lib/components/ui/toggle/index.js b/src/lib/components/ui/toggle/index.js new file mode 100644 index 0000000..71a45e0 --- /dev/null +++ b/src/lib/components/ui/toggle/index.js @@ -0,0 +1,11 @@ +import Root from "./toggle.svelte"; +export { + toggleVariants, + +} from "./toggle.svelte"; + +export { + Root, + // + Root as Toggle, +}; \ No newline at end of file diff --git a/src/lib/components/ui/toggle/toggle.svelte b/src/lib/components/ui/toggle/toggle.svelte new file mode 100644 index 0000000..317b741 --- /dev/null +++ b/src/lib/components/ui/toggle/toggle.svelte @@ -0,0 +1,46 @@ + + + + + \ No newline at end of file diff --git a/src/routes/patient/admission/+page.svelte b/src/routes/patient/admission/+page.svelte index 13b3e99..2ba4f85 100644 --- a/src/routes/patient/admission/+page.svelte +++ b/src/routes/patient/admission/+page.svelte @@ -12,7 +12,6 @@ return await getVisit(row.PVID); }, }); - $inspect(masterDetail.selectedItem)