From b82441d9af871572c72831bd8f31a6d81a23144b Mon Sep 17 00:00:00 2001 From: faiztyanirh Date: Sat, 14 Feb 2026 22:39:01 +0700 Subject: [PATCH] initial dict location done --- .../composable/use-master-detail.svelte.js | 3 +- .../location/config/location-form-config.js | 20 +- .../location/page/create-page.svelte | 8 +- .../dictionary/location/page/edit-page.svelte | 85 +++++++ .../dictionary/location/page/view-page.svelte | 2 +- .../reusable/patient-form-renderer.svelte | 1 - .../form/dictionary-form-renderer.svelte | 233 ++++++++++++++++++ .../reusable/form/form-page-container.svelte | 61 +++++ .../reusable/reusable-data-table.svelte | 14 +- 9 files changed, 409 insertions(+), 18 deletions(-) create mode 100644 src/lib/components/reusable/form/dictionary-form-renderer.svelte create mode 100644 src/lib/components/reusable/form/form-page-container.svelte diff --git a/src/lib/components/composable/use-master-detail.svelte.js b/src/lib/components/composable/use-master-detail.svelte.js index d043226..e6cdeeb 100644 --- a/src/lib/components/composable/use-master-detail.svelte.js +++ b/src/lib/components/composable/use-master-detail.svelte.js @@ -29,7 +29,7 @@ export function useMasterDetail(options = {}) { const isDirty = $derived( JSON.stringify(formState.form) !== JSON.stringify(formSnapshot) ); -// $inspect(formState.form) +// $inspect(formSnapshot) async function select(item) { mode = "view"; @@ -63,6 +63,7 @@ export function useMasterDetail(options = {}) { } function enterEdit(param) { + console.log('f'); if (!selectedItem) return; mode = "edit"; diff --git a/src/lib/components/dictionary/location/config/location-form-config.js b/src/lib/components/dictionary/location/config/location-form-config.js index dd7c624..70d7628 100644 --- a/src/lib/components/dictionary/location/config/location-form-config.js +++ b/src/lib/components/dictionary/location/config/location-form-config.js @@ -5,6 +5,9 @@ import { z } from "zod"; export const locationSchema = z.object({ LocCode: z.string().min(1, "Required"), LocFull: z.string().min(1, "Required"), + Email: 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"), + ZIP: z.string().regex(/^$|^[0-9]+$/, "Can only contain numbers"), }); export const locationInitialForm = { @@ -27,6 +30,9 @@ export const locationInitialForm = { export const locationDefaultErrors = { LocCode: "Required", LocFull: "Required", + Email: null, + Phone: null, + ZIP: null, }; export const locationFormFields = [ @@ -41,7 +47,9 @@ export const locationFormFields = [ label: "Site ID", required: false, type: "select", - optionsEndpoint: `${API.BASE_URL}${API.SITE}`, + optionsEndpoint: `${API.BASE_URL}${API.SITE}`, + valueKey: "SiteID", + labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`, }, { key: "LocCode", @@ -59,7 +67,8 @@ export const locationFormFields = [ key: "LocType", label: "Location Type", required: false, - type: "text", + type: "select", + optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/location_type`, }, { key: "LocFull", @@ -121,6 +130,7 @@ export const locationFormFields = [ label: "ZIP", required: false, type: "text", + validateOn: ["input"] }, ] }, @@ -136,13 +146,15 @@ export const locationFormFields = [ key: "Phone", label: "Phone", required: false, - type: "text" + type: "text", + validateOn: ["input"] }, { key: "Email", label: "Email", required: false, - type: "text", + type: "text", + validateOn: ["input"] } ] }, diff --git a/src/lib/components/dictionary/location/page/create-page.svelte b/src/lib/components/dictionary/location/page/create-page.svelte index c7c5dc8..7a212b0 100644 --- a/src/lib/components/dictionary/location/page/create-page.svelte +++ b/src/lib/components/dictionary/location/page/create-page.svelte @@ -1,7 +1,7 @@ - + import { useDictionaryForm } from "$lib/components/composable/use-dictionary-form.svelte"; + import FormPageContainer from "$lib/components/reusable/form/form-page-container.svelte"; + import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte"; + import { toast } from "svelte-sonner"; + import { untrack } from "svelte"; + import { API } from "$lib/config/api"; + import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte"; + + let props = $props(); + + const { masterDetail, formFields, formActions, schema, initialForm } = props.context; + + const { formState } = masterDetail; + + const helpers = useDictionaryForm(formState); + + let showConfirm = $state(false); + + $effect(() => { + untrack(() => { + formFields.forEach(group => { + group.rows.forEach(row => { + row.columns.forEach(col => { + if (col.type === "group") { + col.columns.forEach(child => { + if (child.type === "select" && child.optionsEndpoint) { + formState.fetchOptions(child, formState.form); + } + }); + } else if ((col.type === "select" || col.type === "identity") && col.optionsEndpoint) { + formState.fetchOptions(col, formState.form); + } + }); + }); + }); + + if (formState.form.Province) { + formState.fetchOptions( + { + key: "City", + optionsEndpoint: `${API.BASE_URL}${API.CITY}`, + dependsOn: "Province", + endpointParamKey: "Parent" + }, + formState.form + ); + } + }); + }); + + async function handleEdit() { + const result = await formState.save(masterDetail.mode); + + if (result.status === 'success') { + console.log('Location updated successfully'); + toast('Location Updated!'); + masterDetail.exitForm(true); + } else { + console.error('Failed to update location:', result.message); + } + } + + const primaryAction = $derived({ + label: 'Edit', + onClick: handleEdit, + disabled: helpers.hasErrors || formState.isSaving.current, + loading: formState.isSaving.current + }); + + const secondaryActions = []; + + + + + + + \ 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 index e556fd1..2bc89a6 100644 --- a/src/lib/components/dictionary/location/page/view-page.svelte +++ b/src/lib/components/dictionary/location/page/view-page.svelte @@ -12,7 +12,7 @@ let location = $derived(masterDetail?.selectedItem?.data); const handlers = { - editPatient: () => masterDetail.enterEdit("data"), + editLocation: () => masterDetail.enterEdit("data"), }; const actions = viewActions(handlers); diff --git a/src/lib/components/patient/reusable/patient-form-renderer.svelte b/src/lib/components/patient/reusable/patient-form-renderer.svelte index 46e780b..3f03f32 100644 --- a/src/lib/components/patient/reusable/patient-form-renderer.svelte +++ b/src/lib/components/patient/reusable/patient-form-renderer.svelte @@ -11,7 +11,6 @@ import CustodianModal from "$lib/components/patient/list/modal/custodian-modal.svelte"; import LinktoModal from "$lib/components/patient/list/modal/linkto-modal.svelte"; import ReusableUpload from "$lib/components/reusable/reusable-upload.svelte"; - import ChevronUpIcon from "@lucide/svelte/icons/chevron-up"; import CheckIcon from "@lucide/svelte/icons/check"; import XIcon from "@lucide/svelte/icons/x"; diff --git a/src/lib/components/reusable/form/dictionary-form-renderer.svelte b/src/lib/components/reusable/form/dictionary-form-renderer.svelte new file mode 100644 index 0000000..9e17f61 --- /dev/null +++ b/src/lib/components/reusable/form/dictionary-form-renderer.svelte @@ -0,0 +1,233 @@ + + +{#snippet Fieldset({ key, label, required, type, optionsEndpoint, options, validateOn, dependsOn, endpointParamKey, valueKey, labelKey })} +
+
+ + {#if required} + * + {/if} +
+ +
+ {#if type === "text"} + { + if (validateOn?.includes("input")) { + formState.validateField(key, formState.form[key], false); + } + }} + onblur={() => { + if (validateOn?.includes("blur")) { + validateFieldAsync(key, mode, originalData?.[key]); + } + }} + /> + {:else if type === "email"} + { + if (validateOn?.includes("input")) { + formState.validateField(key, formState.form[key], false); + } + }} + onblur={() => { + if (validateOn?.includes("blur")) { + formState.validateField(key, formState.form[key], false); + } + }} + /> + {:else if type === "number"} + { + if (validateOn?.includes("input")) { + formState.validateField(key, formState.form[key], false); + } + }} + onblur={() => { + if (validateOn?.includes("blur")) { + formState.validateField(key, formState.form[key], false); + } + }} + /> + {:else if type === "textarea"} + + {:else if type === "select"} + {@const selectedLabel = formState.selectOptions[key]?.find(opt => opt.value === formState.form[key])?.label || "Choose"} + {@const filteredOptions = getFilteredOptions(key)} + { + formState.form[key] = val; + if (validateOn?.includes("input")) { + formState.validateField(key, formState.form[key], false); + } + if (key === "Province") { + formState.form.City = ""; + formState.selectOptions.City = []; + formState.lastFetched.City = null; + } + }} + onOpenChange={(open) => { + if (open && optionsEndpoint) { + formState.fetchOptions( + { key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey }, + formState.form + ); + } + }} + > + + {selectedLabel} + + +
+ +
+ {#if formState.loadingOptions[key]} + Loading... + {:else} + {#if !required} + - None - + {/if} + {#each filteredOptions as option} + + {option.label} + + {/each} + {/if} +
+
+ {:else} + + {/if} + +
+ {#if formState.errors[key]} + + {formState.errors[key]} + + {/if} +
+
+
+{/snippet} + +
+ {#each formFields as group} +
+ {#if group.title} +
+ {group.title} +
+ {/if} + + {#each group.rows as row} +
+ {#each row.columns as col} + {#if col.type === "group"} +
+ {#each col.columns as child} + {@render Fieldset(child)} + {/each} +
+ {:else} + {@render Fieldset(col)} + {/if} + {/each} +
+ {/each} +
+ {/each} +
\ No newline at end of file diff --git a/src/lib/components/reusable/form/form-page-container.svelte b/src/lib/components/reusable/form/form-page-container.svelte new file mode 100644 index 0000000..059dd64 --- /dev/null +++ b/src/lib/components/reusable/form/form-page-container.svelte @@ -0,0 +1,61 @@ + + +
+ + {@render children()} +
+ + {#if secondaryActions.length} + + + + + + + + {#each secondaryActions as action} + + {action.label} + + {/each} + + + + {/if} +
+
\ No newline at end of file diff --git a/src/lib/components/reusable/reusable-data-table.svelte b/src/lib/components/reusable/reusable-data-table.svelte index 1f5a0a7..4d7f04e 100644 --- a/src/lib/components/reusable/reusable-data-table.svelte +++ b/src/lib/components/reusable/reusable-data-table.svelte @@ -118,7 +118,7 @@
-

Rows per page

+

Rows

- + {String(table.getState().pagination.pageSize)} - {#each [1, 2, 3, 4, 5] as pageSize (pageSize)} + {#each [5, 10, 15, 20, 25] as pageSize (pageSize)} {pageSize} @@ -147,7 +147,7 @@