diff --git a/src/lib/components/app-sidebar.svelte b/src/lib/components/app-sidebar.svelte index 2c34fd9..913eb59 100644 --- a/src/lib/components/app-sidebar.svelte +++ b/src/lib/components/app-sidebar.svelte @@ -53,7 +53,7 @@ dictionary: [ { title: "Admission", - url: "/admission", + url: "/dictionary", icon: BookOpenIcon, submenus: [ { @@ -125,14 +125,8 @@ }, { title: "Test", - url: "#", + url: "/testdef", icon: BookOpenIcon, - submenus: [ - { - title: "Test Site", - url: "#", - }, - ], }, ] }; diff --git a/src/lib/components/composable/use-form-state.svelte.js b/src/lib/components/composable/use-form-state.svelte.js index b170672..7289460 100644 --- a/src/lib/components/composable/use-form-state.svelte.js +++ b/src/lib/components/composable/use-form-state.svelte.js @@ -1,7 +1,7 @@ export function useFormState(initial) { const form = $state(structuredClone(initial)) const isSaving = $state({ current: false }); - console.log(form); + // function resetForm() { // Object.assign(form, structuredClone(initial)); // } diff --git a/src/lib/components/composable/use-master-detail.svelte.js b/src/lib/components/composable/use-master-detail.svelte.js index 3ac5182..d043226 100644 --- a/src/lib/components/composable/use-master-detail.svelte.js +++ b/src/lib/components/composable/use-master-detail.svelte.js @@ -29,9 +29,7 @@ export function useMasterDetail(options = {}) { const isDirty = $derived( JSON.stringify(formState.form) !== JSON.stringify(formSnapshot) ); - - $inspect(formState.form) - +// $inspect(formState.form) async function select(item) { mode = "view"; diff --git a/src/lib/components/dictionary/location/api/location-api.js b/src/lib/components/dictionary/location/api/location-api.js new file mode 100644 index 0000000..426f081 --- /dev/null +++ b/src/lib/components/dictionary/location/api/location-api.js @@ -0,0 +1,18 @@ +import { API } from '$lib/config/api.js'; +import { getById, searchWithParams, create, update } from '$lib/api/api-client'; + +export async function getLocations(searchQuery) { + return await searchWithParams(API.LOCATION, searchQuery) +} + +export async function getLocation(searchQuery) { + return await getById(API.LOCATION, searchQuery) +} + +export async function createLocation(newLocationForm) { + return await create(API.LOCATION, newLocationForm) +} + +export async function editLocation(editLocationForm) { + return await update(API.LOCATION, editLocationForm) +} \ No newline at end of file diff --git a/src/lib/components/dictionary/location/config/location-config.js b/src/lib/components/dictionary/location/config/location-config.js new file mode 100644 index 0000000..e638d5e --- /dev/null +++ b/src/lib/components/dictionary/location/config/location-config.js @@ -0,0 +1,48 @@ +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: "LocCode", + label: "Location Code", + type: "text", + }, + { + key: "LocName", + label: "Location Name", + type: "text", + }, + { + key: "LocType", + label: "Location Type", + type: "text" + }, +]; + +export const detailSections = [ +]; + +export function locationActions(masterDetail) { + return [ + { + Icon: PlusIcon, + label: 'Add Location', + onClick: () => masterDetail.enterCreate(), + }, + { + Icon: Settings2Icon, + label: 'Search Parameters', + }, + ]; +} + +export function viewActions(handlers){ + return [ + { + Icon: PencilIcon, + label: 'Edit Location', + onClick: handlers.editLocation, + }, + ] +} \ No newline at end of file diff --git a/src/lib/components/dictionary/location/config/location-form-config.js b/src/lib/components/dictionary/location/config/location-form-config.js new file mode 100644 index 0000000..098afca --- /dev/null +++ b/src/lib/components/dictionary/location/config/location-form-config.js @@ -0,0 +1,165 @@ +import { API } from "$lib/config/api"; +import EraserIcon from "@lucide/svelte/icons/eraser"; +import { z } from "zod"; + +export const locationSchema = z.object({}); + +export const locationInitialForm = { + LocationID: '', + LocCode: '', + LocType: '', + LocFull: '', + SiteID: '', + Street1: '', + Street2: '', + Phone: '', + Email: '', + City: '', + Province: '', + ZIP: '', + GeoLocationSystem: '', + GeoLocationData: '', +}; + +export const locationDefaultErrors = { + LocCode: "Required", + LocName: "Required", + LocType: "Required", +}; + +export const locationFormFields = [ + { + title: "Basic Information", + rows: [ + { + type: "row", + columns: [ + { + key: "SiteID", + label: "Site ID", + required: true, + type: "select", + optionsEndpoint: `${API.BASE_URL}${API.SITE}`, + }, + { + key: "LocCode", + label: "Location Code", + required: true, + type: "text", + validateOn: ["input"] + }, + { + key: "LocType", + label: "Location Type", + required: true, + type: "text", + validateOn: ["input"] + }, + { + key: "LocFull", + label: "Location Name", + required: false, + type: "text", + } + ] + } + ] + }, + { + title: "Address Detail", + rows: [ + { + type: "row", + columns: [ + { + key: "Province", + label: "Province", + required: false, + type: "select", + optionsEndpoint: `${API.BASE_URL}${API.PROVINCE}`, + }, + { + key: "City", + label: "City", + required: false, + type: "select", + optionsEndpoint: `${API.BASE_URL}${API.CITY}`, + }, + { + key: "ZIP", + label: "ZIP", + required: false, + type: "text", + }, + { + key: "Street1", + label: "Street 1", + required: false, + type: "text", + }, + { + key: "Street2", + label: "Street 2", + required: false, + type: "text", + } + ] + }, + + ] + }, + { + title: "Contact Information", + rows: [ + { + type: "row", + columns: [ + { + key: "Phone", + label: "Phone", + required: false, + type: "text" + }, + { + key: "Email", + label: "Email", + required: false, + type: "text", + } + ] + }, + ] + }, + { + title: "Geographic Data", + rows: [ + { + type: "row", + columns: [ + { + key: "GeoLocationSystem", + label: "Geo Location System", + required: false, + type: "text", + }, + { + key: "GeoLocationData", + label: "Geo Location Data", + required: false, + type: "text", + }, + ] + }, + ] + }, +]; + +export function getLocationFormActions(handlers) { + return [ + { + Icon: EraserIcon, + label: 'Clear Form', + onClick: handlers.clearForm, + }, + ]; +} \ No newline at end of file diff --git a/src/lib/components/dictionary/location/page/create-page.svelte b/src/lib/components/dictionary/location/page/create-page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/components/dictionary/location/page/edit-page.svelte b/src/lib/components/dictionary/location/page/edit-page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/components/dictionary/location/page/master-page.svelte b/src/lib/components/dictionary/location/page/master-page.svelte new file mode 100644 index 0000000..28f8927 --- /dev/null +++ b/src/lib/components/dictionary/location/page/master-page.svelte @@ -0,0 +1,65 @@ + + +{#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 "LOCATION".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 search.searchData.length > 0} + + {:else} +
+ +
+ {/if} +
+
+ {/if} +
+
\ No newline at end of file diff --git a/src/lib/components/dictionary/location/page/view-page.svelte b/src/lib/components/dictionary/location/page/view-page.svelte new file mode 100644 index 0000000..3e0b9eb --- /dev/null +++ b/src/lib/components/dictionary/location/page/view-page.svelte @@ -0,0 +1 @@ +vw \ No newline at end of file diff --git a/src/lib/components/dictionary/location/table/location-columns.js b/src/lib/components/dictionary/location/table/location-columns.js new file mode 100644 index 0000000..d63e479 --- /dev/null +++ b/src/lib/components/dictionary/location/table/location-columns.js @@ -0,0 +1,14 @@ +export const locationColumns = [ + { + accessorKey: "LocCode", + header: "Location Code", + }, + { + accessorKey: "LocFull", + header: "Location Name", + }, + { + accessorKey: "LocType", + header: "Location Type", + }, +]; \ No newline at end of file diff --git a/src/lib/components/dictionary/testdef/api/testdef-api.js b/src/lib/components/dictionary/testdef/api/testdef-api.js new file mode 100644 index 0000000..9219a84 --- /dev/null +++ b/src/lib/components/dictionary/testdef/api/testdef-api.js @@ -0,0 +1,18 @@ +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.TEST, searchQuery) +} + +export async function getTest(searchQuery) { + return await getById(API.TEST, searchQuery) +} + +export async function createTest(newTestForm) { + return await create(API.TEST, newTestForm) +} + +export async function editTest(editTestForm) { + return await update(API.TEST, editTestForm) +} \ No newline at end of file diff --git a/src/lib/components/dictionary/testdef/config/testdef-config.js b/src/lib/components/dictionary/testdef/config/testdef-config.js new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/components/dictionary/testdef/config/testdef-form-config.js b/src/lib/components/dictionary/testdef/config/testdef-form-config.js new file mode 100644 index 0000000..94cd262 --- /dev/null +++ b/src/lib/components/dictionary/testdef/config/testdef-form-config.js @@ -0,0 +1,211 @@ +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 testSchema = z.object({}); + +export const admissionInitialForm = { + InternalPVID: "", + InternalPID: "", + PVID: "", + EpisodeID: "", + DiagCode: "", + Diagnosis: "", + ADTCode: "", + LocationID: "", + AttDoc: "", + RefDoc: "", + AdmDoc: "", + CnsDoc: "", + isDischarge: false +}; + +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: "isDischarge", + label: "Admission 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 cleanEmptyStrings(payload); +} \ No newline at end of file diff --git a/src/lib/components/dictionary/testdef/page/create-page.svelte b/src/lib/components/dictionary/testdef/page/create-page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/components/dictionary/testdef/page/edit-page.svelte b/src/lib/components/dictionary/testdef/page/edit-page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/components/dictionary/testdef/page/master-page.svelte b/src/lib/components/dictionary/testdef/page/master-page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/components/dictionary/testdef/page/view-page.svelte b/src/lib/components/dictionary/testdef/page/view-page.svelte new file mode 100644 index 0000000..e69de29 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 4f39a76..0cc2987 100644 --- a/src/lib/components/patient/admission/config/admission-form-config.js +++ b/src/lib/components/patient/admission/config/admission-form-config.js @@ -117,7 +117,7 @@ export const admissionFormFields = [ label: "Visit Class", required: false, type: "select", - optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/visit_classes`, + // optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/visit_classes`, } ] }, @@ -129,7 +129,7 @@ export const admissionFormFields = [ label: "Service Class", required: false, type: "select", - optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/service_classes`, + // optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/service_classes`, }, { key: "isDischarge", diff --git a/src/lib/components/patient/admission/page/create-page.svelte b/src/lib/components/patient/admission/page/create-page.svelte index d6f463d..7e4983f 100644 --- a/src/lib/components/patient/admission/page/create-page.svelte +++ b/src/lib/components/patient/admission/page/create-page.svelte @@ -4,6 +4,7 @@ import PatientFormRenderer from "$lib/components/patient/reusable/patient-form-renderer.svelte"; import { buildPayload } from "$lib/components/patient/admission/config/admission-form-config"; import { toast } from "svelte-sonner"; + import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte"; let props = $props(); @@ -21,6 +22,19 @@ const actions = formActions(handlers); + let showConfirm = $state(false); + + function handleExit() { + const ok = masterDetail.exitForm(); + if (!ok) { + showConfirm = true; + } + } + + function confirmDiscard() { + masterDetail.exitForm(true); + } + async function handleSave() { const payload = buildPayload(formState.form); @@ -28,7 +42,7 @@ console.log(payload); toast('Visit Created!'); - masterDetail?.exitForm(); + masterDetail?.exitForm(true); } const primaryAction = $derived({ @@ -56,4 +70,9 @@ formFields={formFields} mode="create" /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/lib/components/patient/admission/page/edit-page.svelte b/src/lib/components/patient/admission/page/edit-page.svelte index aada9bc..864b9c6 100644 --- a/src/lib/components/patient/admission/page/edit-page.svelte +++ b/src/lib/components/patient/admission/page/edit-page.svelte @@ -5,6 +5,7 @@ import { buildPayload } from "$lib/components/patient/admission/config/admission-form-config"; import { toast } from "svelte-sonner"; import { buildEditPayloadWithDiff } from "$lib/components/patient/admission/config/admission-payload"; + import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte"; let props = $props(); @@ -12,6 +13,19 @@ const { formState } = masterDetail; + let showConfirm = $state(false); + + function handleExit() { + const ok = masterDetail.exitForm(); + if (!ok) { + showConfirm = true; + } + } + + function confirmDiscard() { + masterDetail.exitForm(true); + } + $effect(() => { // const backendData = masterDetail?.selectedItem.data; // if (!backendData) return; @@ -46,8 +60,8 @@ const result = await masterDetail.formState.save(masterDetail.mode, customPayload); console.log(customPayload); - toast('Visit Updated!'); - masterDetail?.exitForm(); + // toast('Visit Updated!'); + // masterDetail?.exitForm(); // const result = await formState.save(); // if (result.status === 'success') { @@ -72,4 +86,9 @@ formFields={formFields} mode="edit" /> - \ No newline at end of file + + + \ No newline at end of file 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 09a113e..ff29293 100644 --- a/src/lib/components/patient/list/config/patient-form-config.js +++ b/src/lib/components/patient/list/config/patient-form-config.js @@ -10,7 +10,7 @@ export const patientSchema = z.object({ (date) => new Date(date) <= new Date(), "Cannot exceed today's date" ), - EmailAddress1: z.string().min(1, "Required").email("Invalid email format"), + EmailAddress1: z.string().trim().optional().refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),"Invalid email format"), EmailAddress2: z.string().trim().optional().refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),"Invalid email format"), Phone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"), MobilePhone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"), @@ -67,7 +67,7 @@ export const patientDefaultErrors = { NameFirst: "Required", Sex: "Required", Birthdate: "Required", - EmailAddress1: "Required", + EmailAddress1: null, EmailAddress2: null, 'PatIdt.Identifier': null, Phone: null, @@ -255,7 +255,7 @@ export const patientFormFields = [ { type: "row", columns: [ - { key: "EmailAddress1", label: "Email Address 1", required: true, type: "email", validateOn: ["input", "blur"] }, + { key: "EmailAddress1", label: "Email Address 1", required: false, type: "email", validateOn: ["input", "blur"] }, { key: "Phone", label: "Phone", required: false, type: "text", validateOn: ["input"] }, ] }, diff --git a/src/lib/components/patient/list/page/edit-page.svelte b/src/lib/components/patient/list/page/edit-page.svelte index 223eccc..8913dff 100644 --- a/src/lib/components/patient/list/page/edit-page.svelte +++ b/src/lib/components/patient/list/page/edit-page.svelte @@ -96,16 +96,16 @@ // $inspect(masterDetail?.selectedItem?.patient) // $inspect(formState.form) async function handleEdit() { - console.log(formState.form); - // const result = await formState.save(masterDetail.mode); + // console.log(formState.form); + const result = await formState.save(masterDetail.mode); - // if (result.status === 'success') { - // console.log('Patient updated successfully'); - // toast('Patient Updated!'); - // masterDetail.exitForm(); - // } else { - // console.error('Failed to update patient:', result.message); - // } + if (result.status === 'success') { + console.log('Patient updated successfully'); + toast('Patient Updated!'); + masterDetail.exitForm(true); + } else { + console.error('Failed to update patient:', result.message); + } } const primaryAction = $derived({ diff --git a/src/lib/config/api.js b/src/lib/config/api.js index 772f9ab..a13779e 100644 --- a/src/lib/config/api.js +++ b/src/lib/config/api.js @@ -25,5 +25,4 @@ export const API = { DEPARTMENT: '/api/organization/department', WORKSTATION: '/api/organization/workstation', TEST: '/api/tests', - TESTSITE: '/api/test/testdefsite', }; diff --git a/src/routes/dictionary/location/+page.svelte b/src/routes/dictionary/location/+page.svelte new file mode 100644 index 0000000..5987f50 --- /dev/null +++ b/src/routes/dictionary/location/+page.svelte @@ -0,0 +1,51 @@ + + +
+ {#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 diff --git a/src/routes/dictionary/testdef/+page.svelte b/src/routes/dictionary/testdef/+page.svelte new file mode 100644 index 0000000..5b65cb6 --- /dev/null +++ b/src/routes/dictionary/testdef/+page.svelte @@ -0,0 +1,60 @@ + + +
+ {#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