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