From ec14173256dde6ee2a2d6d17497b35f2abbc3151 Mon Sep 17 00:00:00 2001 From: faiztyanirh Date: Mon, 6 Apr 2026 21:28:30 +0700 Subject: [PATCH] refactor create contact ui --- .../contact/config/contact-form-config.js | 55 +++- .../contact/page/create-page.svelte | 174 +++++++++- .../contact/page/edit-page copy.svelte | 309 ++++++++++++++++++ .../dictionary/contact/page/edit-page.svelte | 59 +++- .../testmap/page/create-page.svelte | 7 +- .../admission/config/admission-config.js | 2 +- .../patient/list/page/edit-page.svelte | 11 +- src/lib/utils/getChangedFields.js | 9 + 8 files changed, 589 insertions(+), 37 deletions(-) create mode 100644 src/lib/components/dictionary/contact/page/edit-page copy.svelte create mode 100644 src/lib/utils/getChangedFields.js diff --git a/src/lib/components/dictionary/contact/config/contact-form-config.js b/src/lib/components/dictionary/contact/config/contact-form-config.js index f13406c..2cd8d64 100644 --- a/src/lib/components/dictionary/contact/config/contact-form-config.js +++ b/src/lib/components/dictionary/contact/config/contact-form-config.js @@ -1,10 +1,21 @@ import { API } from "$lib/config/api"; +import { Contact } from "@lucide/svelte"; import EraserIcon from "@lucide/svelte/icons/eraser"; import { z } from "zod"; +import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings"; export const contactSchema = z.object({ NameFirst: z.string().min(1, "Required"), Initial: z.string().min(1, "Required"), + 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"), + MobilePhone1: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"), + MobilePhone2: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"), + Phone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"), +}); + +export const contactDetailSchema = z.object({ + ContactEmail: z.string().trim().optional().refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),"Invalid email format"), }); export const contactInitialForm = { @@ -24,8 +35,8 @@ export const contactInitialForm = { }; export const contactDetailInitialForm = { - SiteID: '', ContactDetID: '', + SiteID: '', ContactCode: '', ContactEmail: '', OccupationID: '', @@ -36,6 +47,15 @@ export const contactDetailInitialForm = { export const contactDefaultErrors = { NameFirst: "Required", Initial: "Required", + EmailAddress1: null, + EmailAddress2: null, + MobilePhone1: null, + MobilePhone2: null, + Phone: null, +}; + +export const contactDetailDefaultErrors = { + ContactEmail: null, }; export const contactFormFields = [ @@ -102,13 +122,15 @@ export const contactFormFields = [ key: "EmailAddress1", label: "Email Address 1", required: false, - type: "text", + type: "email", + validateOn: ["input"], }, { key: "EmailAddress2", label: "Email Address 2", required: false, - type: "text", + type: "email", + validateOn: ["input"], }, ] }, @@ -119,19 +141,22 @@ export const contactFormFields = [ key: "MobilePhone1", label: "Mobile Phone 1", required: false, - type: "text", + type: "text", + validateOn: ["input"], }, { key: "MobilePhone2", label: "Mobile Phone 2", required: false, - type: "text", + type: "text", + validateOn: ["input"], }, { key: "Phone", label: "Phone", required: false, type: "text", + validateOn: ["input"], }, ] }, @@ -163,6 +188,7 @@ export const contactFormFields = [ export const contactDetailFormFields = [ { + title: "Contact Detail Information", rows: [ { type: "row", @@ -221,4 +247,23 @@ export function getContactFormActions(handlers) { onClick: handlers.clearForm, }, ]; +} + +export function buildContactPayload({ + mainForm, + tempDetailContact, +}) { + let payload = { + ...mainForm, + Details: tempDetailContact.map((item) => ({ + SiteID: item.SiteID, + ContactCode: item.ContactCode, + ContactEmail: item.ContactEmail, + Department: item.Department, + OccupationID: item.OccupationID, + JobTitle: item.JobTitle, + })), + }; + + return cleanEmptyStrings(payload); } \ No newline at end of file diff --git a/src/lib/components/dictionary/contact/page/create-page.svelte b/src/lib/components/dictionary/contact/page/create-page.svelte index b2b1ef3..3973d3b 100644 --- a/src/lib/components/dictionary/contact/page/create-page.svelte +++ b/src/lib/components/dictionary/contact/page/create-page.svelte @@ -4,6 +4,14 @@ import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte"; import { toast } from "svelte-sonner"; import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte"; + import { useForm } from "$lib/components/composable/use-form.svelte"; + import { contactDetailSchema, contactDetailInitialForm, contactDetailDefaultErrors, contactDetailFormFields, buildContactPayload } from "$lib/components/dictionary/contact/config/contact-form-config"; + import { Separator } from '$lib/components/ui/separator/index.js'; + import { Button } from '$lib/components/ui/button/index.js'; + import * as Table from '$lib/components/ui/table/index.js'; + import PencilIcon from "@lucide/svelte/icons/pencil"; + import Trash2Icon from "@lucide/svelte/icons/trash-2"; + import { untrack } from "svelte"; let props = $props(); @@ -11,11 +19,23 @@ const { formState } = masterDetail; + let editingId = $state(null); + let idCounter = $state(0); + let tempDetailContact = $state([]); + + const contactDetailFormState = useForm({ + schema: contactDetailSchema, + initialForm: contactDetailInitialForm, + defaultErrors: contactDetailDefaultErrors, + }); + const helpers = useDictionaryForm(formState); const handlers = { clearForm: () => { formState.reset(); + contactDetailFormState.reset(); + tempDetailContact = []; } }; @@ -23,11 +43,80 @@ let showConfirm = $state(false); - async function handleSave() { - const result = await formState.save(masterDetail.mode); + function snapshotForm() { + return untrack(() => { + const f = contactDetailFormState.form; + return { + SiteID: f.SiteID ?? "", + ContactCode: f.ContactCode ?? "", + ContactEmail: f.ContactEmail ?? "", + Department: f.Department ?? "", + OccupationID: f.OccupationID ?? "", + JobTitle: f.JobTitle ?? "", + }; + }); + } - toast('Contact Created!'); - masterDetail?.exitForm(true); + function resetContactDetailForm() { + contactDetailFormState.reset(); + editingId = null; + } + + function handleInsertDetail() { + const row = { + id: ++idCounter, + ...snapshotForm() + }; + + tempDetailContact = [...tempDetailContact, row]; + + resetContactDetailForm(); + } + + async function handleEditDetail(row) { + editingId = row.id; + + untrack(() => { + const f = contactDetailFormState.form; + + f.SiteID = row.SiteID; + f.ContactCode = row.ContactCode; + f.ContactEmail = row.ContactEmail; + f.Department = row.Department; + f.OccupationID = row.OccupationID; + f.JobTitle = row.JobTitle; + }); + } + + function handleUpdateDetail() { + tempDetailContact = tempDetailContact.map((row) => + row.id === editingId ? { id: row.id, ...snapshotForm() } : row + ); + resetContactDetailForm(); + } + + function handleCancelEditDetail() { + resetContactDetailForm(); + } + + function handleRemoveDetail(id) { + tempDetailContact = tempDetailContact.filter((row) => row.id !== id); + if (editingId === id) { + resetContactDetailForm(); + } + } + + async function handleSave() { + const mainForm = masterDetail.formState.form; + const payload = buildContactPayload({ + mainForm, + tempDetailContact, + }); + console.log(payload) + // const result = await formState.save(masterDetail.mode); + + // toast('Contact Created!'); + // masterDetail?.exitForm(true); } const primaryAction = $derived({ @@ -46,6 +135,83 @@ formFields={formFields} mode="create" /> + + + +
+ +
+ {#if editingId !== null} + + + {:else} + + {/if} +
+
+ +
+ + + + + Site + Code + Department + Occupation + Job Title + Email + + + + + {#if tempDetailContact.length === 0} + + + No data. Fill the form above and click Insert. + + + {:else} + {#each tempDetailContact as row (row.id)} + + {row.SiteID} + {row.ContactCode} + {row.Department} + {row.OccupationID} + {row.JobTitle} + {row.ContactEmail} + +
+ + +
+
+
+ {/each} + {/if} +
+
+
+ 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"; + import { Separator } from "$lib/components/ui/separator/index.js"; + import PlusIcon from "@lucide/svelte/icons/plus"; + import * as Card from "$lib/components/ui/card/index.js"; + import { Badge } from "$lib/components/ui/badge/index.js"; + import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte"; + import { contactDetailInitialForm, contactDetailFormFields } from "$lib/components/dictionary/contact/config/contact-form-config"; + import { Button } from "$lib/components/ui/button/index.js"; + import { useForm } from "$lib/components/composable/use-form.svelte"; + import XIcon from "@lucide/svelte/icons/x"; + import Edit2Icon from "@lucide/svelte/icons/edit-2"; + import { getChangedFields } from "$lib/utils/getChangedFields"; + + let props = $props(); + + const { masterDetail, formFields, formActions, schema, initialForm } = props.context; + + const { formState } = masterDetail; + + const detailFormState = useForm({ + schema: null, + initialForm: contactDetailInitialForm, + defaultErrors: {}, + mode: 'create', + modeOpt: 'default', + saveEndpoint: null, + editEndpoint: null, + }); + + let showDetailForm = $state(false); + + let editingDetailIndex = $state(null); + + let isEditingDetail = $derived(editingDetailIndex !== null); + + const helpers = useDictionaryForm(formState); + + let showConfirm = $state(false); + + function getLabel(fieldKey, value) { + if (!detailFormState.selectOptions?.[fieldKey]) return value; + const option = detailFormState.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) { + detailFormState.fetchOptions(col, detailFormState.form); + } + }); + }); + }); + }); + }); + let editedDetails = $state(masterDetail.selectedItem?.data?.Details || []); + + async function handleEdit() { + 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 = []; + + const actionsDetail = [ + { + Icon: PlusIcon, + label: 'Add Contact Detail', + onClick: () => addDetail(), + }, + ]; + + function addDetail() { + editingDetailIndex = null; // Mode create baru + detailFormState.reset(); // Reset form ke initialForm + detailFormState.setForm({ ...contactDetailInitialForm }); // Set form kosong + showDetailForm = true; + } + + async function saveDetail() { + // Ambil current form dari detailFormState.form + const newDetail = { ...detailFormState.form }; + + if (isEditingDetail) { + // Mode edit: update detail yang ada + masterDetail.selectedItem.data.Details[editingDetailIndex] = newDetail; + toast('Contact Detail Updated!'); + } else { + // Mode create: tambah detail baru + if (!masterDetail.selectedItem.data.Details) { + masterDetail.selectedItem.data.Details = []; + } + masterDetail.selectedItem.data.Details.push(newDetail); + toast('Contact Detail Added!'); + } + + // Reset form dan tutup form + detailFormState.reset(); + detailFormState.setForm({ ...contactDetailInitialForm }); + editingDetailIndex = null; + showDetailForm = false; + } + + function editDetail(index) { + const detailToEdit = masterDetail.selectedItem.data.Details[index]; + editingDetailIndex = index; // Set mode edit dengan index + detailFormState.setForm({ ...detailToEdit }); // Load data ke form + showDetailForm = true; + } + + function cancelDetail() { + detailFormState.reset(); + detailFormState.setForm({ ...contactDetailInitialForm }); + editingDetailIndex = null; + showDetailForm = false; + } + + function removeDetail(index) { + masterDetail.selectedItem.data.Details = masterDetail.selectedItem.data.Details.filter((_, i) => i !== index); + toast('Contact Detail Removed!'); + + // Jika sedang mengedit detail yang dihapus, reset form + if (editingDetailIndex === index) { + cancelDetail(); + } else if (editingDetailIndex !== null && editingDetailIndex > index) { + // Adjust index jika mengedit detail setelah yang dihapus + editingDetailIndex--; + } + } + + + + + +
+ +
+ {#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 77af766..f972fe2 100644 --- a/src/lib/components/dictionary/contact/page/edit-page.svelte +++ b/src/lib/components/dictionary/contact/page/edit-page.svelte @@ -16,6 +16,7 @@ import { useForm } from "$lib/components/composable/use-form.svelte"; import XIcon from "@lucide/svelte/icons/x"; import Edit2Icon from "@lucide/svelte/icons/edit-2"; + import { getChangedFields } from "$lib/utils/getChangedFields"; let props = $props(); @@ -43,6 +44,10 @@ let showConfirm = $state(false); + let editingId = $state(null); + let idCounter = $state(0); + let tempMap = $state([]); + function getLabel(fieldKey, value) { if (!detailFormState.selectOptions?.[fieldKey]) return value; const option = detailFormState.selectOptions[fieldKey].find(opt => opt.value === value); @@ -83,21 +88,49 @@ }); }); - async function handleEdit() { - const customPayload = { - ...formState.form, - Details: masterDetail.selectedItem?.data?.Details || [] - }; + let editedDetails = $state(masterDetail.selectedItem?.data?.Details || []); - const result = await formState.save(masterDetail.mode, customPayload); + async function handleEdit() { + 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); - 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 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({ diff --git a/src/lib/components/dictionary/testmap/page/create-page.svelte b/src/lib/components/dictionary/testmap/page/create-page.svelte index c3824e4..e367364 100644 --- a/src/lib/components/dictionary/testmap/page/create-page.svelte +++ b/src/lib/components/dictionary/testmap/page/create-page.svelte @@ -125,10 +125,10 @@ tempMap, }); console.log(payload) - const result = await formState.save(masterDetail.mode, payload); + // const result = await formState.save(masterDetail.mode, payload); - toast('Test Map Created!'); - masterDetail?.exitForm(true); + // toast('Test Map Created!'); + // masterDetail?.exitForm(true); } const primaryAction = $derived({ @@ -223,7 +223,6 @@ // formState.form.ClientTestName = 'nyaho'; // } // }) - $inspect(mapFormFieldsTransformed) diff --git a/src/lib/components/patient/admission/config/admission-config.js b/src/lib/components/patient/admission/config/admission-config.js index 7936fa5..58e9c1c 100644 --- a/src/lib/components/patient/admission/config/admission-config.js +++ b/src/lib/components/patient/admission/config/admission-config.js @@ -103,7 +103,7 @@ export function viewActions(handlers){ return [ { Icon: PencilIcon, - label: 'Edit Patient', + label: 'Edit Visit', onClick: handlers.editPatient, }, ] diff --git a/src/lib/components/patient/list/page/edit-page.svelte b/src/lib/components/patient/list/page/edit-page.svelte index da446b3..03c7c2a 100644 --- a/src/lib/components/patient/list/page/edit-page.svelte +++ b/src/lib/components/patient/list/page/edit-page.svelte @@ -7,6 +7,7 @@ import { API } from "$lib/config/api"; import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte"; import { buildPatientPayload } from "$lib/components/patient/list/config/patient-form-config"; + import { getChangedFields } from "$lib/utils/getChangedFields"; let props = $props(); @@ -60,16 +61,6 @@ } }); }); - - 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 currentPayload = buildPatientPayload(formState.form); diff --git a/src/lib/utils/getChangedFields.js b/src/lib/utils/getChangedFields.js new file mode 100644 index 0000000..1a859fd --- /dev/null +++ b/src/lib/utils/getChangedFields.js @@ -0,0 +1,9 @@ +export 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; +} \ No newline at end of file