diff --git a/src/lib/components/composable/use-search.svelte.js b/src/lib/components/composable/use-search.svelte.js index 5a402d9..f733c69 100644 --- a/src/lib/components/composable/use-search.svelte.js +++ b/src/lib/components/composable/use-search.svelte.js @@ -17,7 +17,7 @@ export function useSearch(searchFields, searchApiFunction) { return query; } - async function handleSearch() { + async function handleSearch(onDone) { isLoading = true; try { searchData = await searchApiFunction(searchQuery); @@ -25,6 +25,7 @@ export function useSearch(searchFields, searchApiFunction) { console.error('Search failed:', error); } finally { isLoading = false; + onDone?.(); } } diff --git a/src/lib/components/dictionary/contact/config/contact-config.js b/src/lib/components/dictionary/contact/config/contact-config.js index 274e307..1b07521 100644 --- a/src/lib/components/dictionary/contact/config/contact-config.js +++ b/src/lib/components/dictionary/contact/config/contact-config.js @@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus"; import Settings2Icon from "@lucide/svelte/icons/settings-2"; import PencilIcon from "@lucide/svelte/icons/pencil"; import { API } from "$lib/config/api"; +import RefreshIcon from "@lucide/svelte/icons/refresh-cw"; export const searchFields = [ { @@ -53,8 +54,13 @@ export const detailSections = [ } ]; -export function contactActions(masterDetail) { +export function contactActions(masterDetail, handlers) { return [ + { + Icon: RefreshIcon, + label: 'Refresh Data', + onClick: handlers.refresh, + }, { Icon: PlusIcon, label: 'Add Contact', diff --git a/src/lib/components/dictionary/contact/page/create-page.svelte b/src/lib/components/dictionary/contact/page/create-page.svelte index 3a63272..db0742b 100644 --- a/src/lib/components/dictionary/contact/page/create-page.svelte +++ b/src/lib/components/dictionary/contact/page/create-page.svelte @@ -113,10 +113,16 @@ tempDetailContact, }); console.log(payload) - const result = await formState.save(masterDetail.mode); + const result = await formState.save(masterDetail.mode, payload); - toast('Contact Created!'); - masterDetail?.exitForm(true); + if (result.status === 'success') { + toast('Contact Created!'); + masterDetail?.exitForm(true); + } else { + console.error('Failed to save contact'); + const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save contact'; + toast.error(errorMessages) + } } const primaryAction = $derived({ diff --git a/src/lib/components/dictionary/contact/page/edit-page copy.svelte b/src/lib/components/dictionary/contact/page/edit-page copy.svelte deleted file mode 100644 index 16c3a1e..0000000 --- a/src/lib/components/dictionary/contact/page/edit-page copy.svelte +++ /dev/null @@ -1,309 +0,0 @@ - - - - - -
- -
- {#if showDetailForm} - - - - - - - - - - {/if} - {#each masterDetail.selectedItem?.data?.Details as contactdetail, index} - - -
-
- - {contactdetail.ContactCode || "null"} - - - {contactdetail.ContactEmail || "null"} - -
-
- - {getLabel('SiteID', contactdetail.SiteID)} - -
- - -
-
-
-
- -
-
-

- Department -

-

- {getLabel('Department', contactdetail.Department)} -

-
-
-

- Job Title -

-

- {contactdetail.JobTitle || "-"} -

-
-
-

- Occupation -

-

- - {getLabel('OccupationID', contactdetail.OccupationID)} -

-
-
-
-
- {/each} -
-
-
- - \ No newline at end of file diff --git a/src/lib/components/dictionary/contact/page/edit-page.svelte b/src/lib/components/dictionary/contact/page/edit-page.svelte index 8e825ee..ddd1d25 100644 --- a/src/lib/components/dictionary/contact/page/edit-page.svelte +++ b/src/lib/components/dictionary/contact/page/edit-page.svelte @@ -21,6 +21,11 @@ const { formState } = masterDetail; + let editingId = $state(null); + let idCounter = $state(0); + let tempDetailContact = $state([]); + let deletedDetailIds = $state([]); + const contactDetailFormState = useForm({ schema: contactDetailSchema, initialForm: contactDetailInitialForm, @@ -31,158 +36,7 @@ let showConfirm = $state(false); - let editingId = $state(null); - let idCounter = $state(0); - let tempDetailContact = $state([]); - let deletedDetailIds = $state([]); - - function getLabel(fieldKey, value) { - if (!contactDetailFormState.selectOptions?.[fieldKey]) return value; - const option = contactDetailFormState.selectOptions[fieldKey].find(opt => opt.value === value); - return option?.label || value || "-"; - } - - $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.optionsEndpoint) { - formState.fetchOptions(col, formState.form); - } - }); - }); - }); - }); - }); - - $effect(() => { - untrack(() => { - contactDetailFormFields.forEach(group => { - group.rows.forEach(row => { - row.columns.forEach(col => { - if (col.type === "select" && col.optionsEndpoint) { - contactDetailFormState.fetchOptions(col, contactDetailFormState.form); - } - }); - }); - }); - }); - }); - - // let editedDetails = $state(masterDetail.selectedItem?.data?.Details || []); - - function diffDetails(current, original) { - const originalMap = new Map( - original.map(item => [item.ContactDetID, item]) - ); - - const updated = []; - - for (const item of current) { - const orig = originalMap.get(item.ContactDetID); - if (!orig) continue; - - const changed = Object.keys(item).some( - key => item[key] !== orig[key] - ); - - if (changed) updated.push(item); - } - - return updated; - } - - async function handleEdit() { - const currentPayload = buildContactPayload({ - mainForm: formState.form, - tempDetailContact - }); - const originalPayload = buildContactPayload({ - mainForm: masterDetail.formSnapshot, - tempDetailContact: masterDetail.formSnapshot.Details - }); - const updatedDetails = diffDetails( - currentPayload.Details, - originalPayload.Details - ); - const finalPayload = { - ...getChangedFields(originalPayload, currentPayload), - Details: { - created: tempDetailContact.filter(r => !r.ContactDetID), - updated: updatedDetails, - deleted: deletedDetailIds.map(id => ({ - ContactDetID: id - })) - } - }; - - console.log(finalPayload); - // console.log('Original Payload:', JSON.stringify(originalPayload)); - // console.log('Current Payload:', JSON.stringify(currentPayload)); - // console.log('Changed Fields:', getChangedFields(originalPayload, currentPayload)); - // console.log(originalPayload.Details); - // console.log(currentPayload.Details); - // console.log('Diff:', diffDetails(currentPayload.Details, originalPayload.Details)); - - // const originalDetails = masterDetail.selectedItem?.data?.Details || []; - - // const currentPayload = $state.snapshot({ ...formState.form, Details: editedDetails }); - // const originalPayload = $state.snapshot({ ...masterDetail.formSnapshot, Details: originalDetails }); - // console.log('Current Payload:', editedDetails); - // console.log('Original Payload:', originalDetails); - - // // const customPayload = { - // // ...formState.form, - // // Details: masterDetail.selectedItem?.data?.Details || [] - // // }; - // // console.log('Custom Payload for Edit:', JSON.stringify(customPayload)); - // // const result = await formState.save(masterDetail.mode, customPayload); - - // // *** - // const changedFields = getChangedFields(originalPayload, currentPayload); - // console.log('Changed Fields:', JSON.stringify(changedFields)); - - // // if (Object.keys(changedFields).length === 0) { - // // toast('No changes detected'); - // // return; - // // } - - // // const payload = { - // // ContactID: formState.form.ContactID, - // // ...changedFields - // // }; - - // // console.log('Custom Payload for Edit:', payload); - // // *** - - // // const result = await formState.save(masterDetail.mode, payload); - - // // if (result.status === 'success') { - // // console.log('Contact updated successfully'); - // // toast('Contact Updated!'); - // // masterDetail.exitForm(true); - // // } else { - // // console.error('Failed to update contact:', result.message); - // // } - } - - const primaryAction = $derived({ - label: 'Edit', - onClick: handleEdit, - disabled: helpers.hasErrors || formState.isSaving.current, - loading: formState.isSaving.current - }); - - const secondaryActions = []; - - function snapshotForm() { + function snapshotForm() { return untrack(() => { const f = contactDetailFormState.form; return { @@ -228,17 +82,6 @@ }); } - // function handleUpdateDetail() { - // tempDetailContact = tempDetailContact.map((row) => - // row.id === editingId ? - // { - // ...row, - // ...snapshotForm() - // } : row - // ); - // resetContactDetailForm(); - // } - function handleUpdateDetail() { const updated = snapshotForm(); @@ -270,6 +113,144 @@ } } + function getLabel(fieldKey, value) { + if (!contactDetailFormState.selectOptions?.[fieldKey]) return value; + const option = contactDetailFormState.selectOptions[fieldKey].find(opt => opt.value === value); + return option?.label || value || "-"; + } + + $effect(() => { + untrack(() => { + formFields.forEach(group => { + group.rows.forEach(row => { + row.columns.forEach(col => { + if ((col.type === "select") && col.optionsEndpoint) { + formState.fetchOptions(col, formState.form); + } + }); + }); + }); + }); + }); + + $effect(() => { + untrack(() => { + contactDetailFormFields.forEach(group => { + group.rows.forEach(row => { + row.columns.forEach(col => { + if ((col.type === "select") && col.optionsEndpoint) { + contactDetailFormState.fetchOptions(col, contactDetailFormState.form); + } + }); + }); + }); + }); + }); + + // function diffDetails(current, original) { + // const originalMap = new Map( + // original.map(item => [item.ContactDetID, item]) + // ); + + // const updated = []; + + // for (const item of current) { + // const orig = originalMap.get(item.ContactDetID); + // if (!orig) continue; + + // const changed = Object.keys(item).some( + // key => item[key] !== orig[key] + // ); + + // if (changed) updated.push(item); + // } + + // return updated; + // } + + function diffDetails(currentRows, originalRows) { + const originalMap = new Map( + originalRows + .filter(item => item.ContactDetID) + .map(item => [item.ContactDetID, item]) + ); + + const updated = []; + const detailKeys = ['SiteID', 'ContactCode', 'ContactEmail', 'OccupationID', 'JobTitle', 'Department']; + + for (const item of currentRows) { + if (!item.ContactDetID) continue; + + const orig = originalMap.get(item.ContactDetID); + if (!orig) continue; + + const changed = detailKeys.some( + key => item[key] !== orig[key] + ); + + if (changed) updated.push(item); + } + + return updated; + } + + async function handleEdit() { + const currentPayload = buildContactPayload({ + mainForm: formState.form, + tempDetailContact + }); + const originalPayload = buildContactPayload({ + mainForm: masterDetail.formSnapshot, + tempDetailContact: masterDetail.formSnapshot.Details ?? [] + }); + const originalRows = masterDetail.formSnapshot.Details ?? []; + const updatedDetails = diffDetails(tempDetailContact, originalRows); + + const changedFields = getChangedFields(originalPayload, currentPayload); + const hasMainChanges = Object.keys(changedFields).length > 0; + const hasDetailChanges = updatedDetails.length > 0 || tempDetailContact.some(r => !r.ContactDetID) || deletedDetailIds.length > 0; + + if (!hasMainChanges && !hasDetailChanges) { + toast('No changes detected'); + return; + } + + const finalPayload = { + ContactID: formState.form.ContactID, + ...changedFields, + ...(hasDetailChanges && { + Details: { + created: tempDetailContact.filter(r => !r.ContactDetID), + edited: updatedDetails, + deleted: deletedDetailIds + } + }) + }; + + console.log(finalPayload); + + const result = await formState.save(masterDetail.mode, finalPayload); + + if (result.status === 'success') { + console.log('Contact updated successfully'); + toast('Contact Updated!'); + masterDetail.exitForm(true); + } else { + console.error('Failed to update contact:', result.message); + const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to update contact'; + toast.error(errorMessages) + } + } + + const primaryAction = $derived({ + label: 'Edit', + onClick: handleEdit, + disabled: helpers.hasErrors || formState.isSaving.current, + loading: formState.isSaving.current + }); + + const secondaryActions = []; + $effect(() => { const mainForm = formState.form; if (mainForm.Details && Array.isArray(mainForm.Details)) { diff --git a/src/lib/components/dictionary/contact/page/master-page.svelte b/src/lib/components/dictionary/contact/page/master-page.svelte index 8f064bd..eff5826 100644 --- a/src/lib/components/dictionary/contact/page/master-page.svelte +++ b/src/lib/components/dictionary/contact/page/master-page.svelte @@ -8,12 +8,15 @@ import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte"; import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte"; import UserXIcon from "@lucide/svelte/icons/user-x"; + import MoveLeftIcon from "@lucide/svelte/icons/move-left"; let props = $props(); const search = useSearch(searchFields, getContacts); - const initialForm = props.masterDetail.formState.form; - const actions = contactActions(props.masterDetail, initialForm) + const handlers = { + refresh: () => {search.handleSearch()}, + }; + const actions = contactActions(props.masterDetail, handlers) actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet; let activeRowId = $state(null); @@ -38,10 +41,13 @@ >
{#if props.masterDetail.isFormMode} - - {#each "CONTACT".split("") as c} - {c} - {/each} + + +
+ {#each "CONTACT".split("") as c} + {c} + {/each} +
{/if} diff --git a/src/lib/components/dictionary/location/config/location-config.js b/src/lib/components/dictionary/location/config/location-config.js index c6883f2..b604612 100644 --- a/src/lib/components/dictionary/location/config/location-config.js +++ b/src/lib/components/dictionary/location/config/location-config.js @@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus"; import Settings2Icon from "@lucide/svelte/icons/settings-2"; import PencilIcon from "@lucide/svelte/icons/pencil"; import { API } from "$lib/config/api"; +import RefreshIcon from "@lucide/svelte/icons/refresh-cw"; export const searchFields = [ { @@ -67,8 +68,13 @@ export const detailSections = [ }, ]; -export function locationActions(masterDetail) { +export function locationActions(masterDetail, handlers) { return [ + { + Icon: RefreshIcon, + label: 'Refresh Data', + onClick: handlers.refresh, + }, { Icon: PlusIcon, label: 'Add Location', diff --git a/src/lib/components/dictionary/location/page/master-page.svelte b/src/lib/components/dictionary/location/page/master-page.svelte index d9213b9..8de7230 100644 --- a/src/lib/components/dictionary/location/page/master-page.svelte +++ b/src/lib/components/dictionary/location/page/master-page.svelte @@ -8,12 +8,15 @@ import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte"; import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte"; import MapPinXIcon from "@lucide/svelte/icons/map-pin-x"; + import MoveLeftIcon from "@lucide/svelte/icons/move-left"; let props = $props(); const search = useSearch(searchFields, getLocations); - const initialForm = props.masterDetail.formState.form; - const actions = locationActions(props.masterDetail, initialForm) + const handlers = { + refresh: () => {search.handleSearch()}, + }; + const actions = locationActions(props.masterDetail, handlers) actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet; let activeRowId = $state(null); @@ -38,10 +41,13 @@ >
{#if props.masterDetail.isFormMode} - - {#each "LOCATION".split("") as c} - {c} - {/each} + + +
+ {#each "LOCATION".split("") as c} + {c} + {/each} +
{/if} diff --git a/src/lib/components/dictionary/occupation/config/occupation-config.js b/src/lib/components/dictionary/occupation/config/occupation-config.js index 738871e..769c437 100644 --- a/src/lib/components/dictionary/occupation/config/occupation-config.js +++ b/src/lib/components/dictionary/occupation/config/occupation-config.js @@ -1,6 +1,7 @@ import PlusIcon from "@lucide/svelte/icons/plus"; import Settings2Icon from "@lucide/svelte/icons/settings-2"; import PencilIcon from "@lucide/svelte/icons/pencil"; +import RefreshIcon from "@lucide/svelte/icons/refresh-cw"; export const searchFields = [ { @@ -37,8 +38,13 @@ export const detailSections = [ }, ]; -export function occupationActions(masterDetail) { +export function occupationActions(masterDetail, handlers) { return [ + { + Icon: RefreshIcon, + label: 'Refresh Data', + onClick: handlers.refresh, + }, { Icon: PlusIcon, label: 'Add Location', diff --git a/src/lib/components/dictionary/occupation/page/master-page.svelte b/src/lib/components/dictionary/occupation/page/master-page.svelte index 83727aa..92e362d 100644 --- a/src/lib/components/dictionary/occupation/page/master-page.svelte +++ b/src/lib/components/dictionary/occupation/page/master-page.svelte @@ -8,20 +8,23 @@ import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte"; import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte"; import FolderXIcon from "@lucide/svelte/icons/folder-x"; + import MoveLeftIcon from "@lucide/svelte/icons/move-left"; let props = $props(); const search = useSearch(searchFields, getOccupations); - const initialForm = props.masterDetail.formState.form; - const actions = occupationActions(props.masterDetail, initialForm) + const handlers = { + refresh: () => {search.handleSearch()}, + }; + const actions = occupationActions(props.masterDetail, handlers) actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet; let activeRowId = $state(null); -{#snippet searchParamSnippet()} +{#snippet searchParamSnippet(close)} search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading} selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions} /> {/snippet} @@ -38,10 +41,13 @@ >
{#if props.masterDetail.isFormMode} - - {#each "OCCUPATION".split("") as c} - {c} - {/each} + + +
+ {#each "OCCUPATION".split("") as c} + {c} + {/each} +
{/if} diff --git a/src/lib/components/dictionary/occupation/page/view-page.svelte b/src/lib/components/dictionary/occupation/page/view-page.svelte index 7ca1cf1..1b4f386 100644 --- a/src/lib/components/dictionary/occupation/page/view-page.svelte +++ b/src/lib/components/dictionary/occupation/page/view-page.svelte @@ -4,6 +4,7 @@ import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte"; import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte"; import FolderXIcon from "@lucide/svelte/icons/folder-x"; + import { Spinner } from "$lib/components/ui/spinner/index.js"; let props = $props(); @@ -46,7 +47,11 @@
{/snippet} -{#if masterDetail.selectedItem} +{#if masterDetail.isLoadingDetail} +
+ +
+{:else if masterDetail.selectedItem}
diff --git a/src/lib/components/patient/list/page/view-page.svelte b/src/lib/components/patient/list/page/view-page.svelte index b5d93aa..0b499bc 100644 --- a/src/lib/components/patient/list/page/view-page.svelte +++ b/src/lib/components/patient/list/page/view-page.svelte @@ -42,8 +42,14 @@ .filter(val => val && val.trim() !== "") .join(" / "); } + let value = field.parentKey ? patient[field.parentKey]?.[field.key] : patient[field.key]; - return field.parentKey ? patient[field.parentKey]?.[field.key] : patient[field.key]; + if (field.valueMap && value != null) { + value = field.valueMap[value] ?? value; + } + + // return field.parentKey ? patient[field.parentKey]?.[field.key] : patient[field.key]; + return value; } diff --git a/src/lib/components/topbar/topbar-action.svelte b/src/lib/components/topbar/topbar-action.svelte index 37bee19..a2a4b87 100644 --- a/src/lib/components/topbar/topbar-action.svelte +++ b/src/lib/components/topbar/topbar-action.svelte @@ -7,6 +7,8 @@ let props = $props(); const { Icon } = props; + + let open = $state(false); @@ -14,7 +16,7 @@ {#snippet child({ props: tooltipProps })} {#if props.popoverContent} - + {#snippet child({ props: popoverProps })}