mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-26 10:56:32 +07:00
refactor create contact ui
This commit is contained in:
parent
45a6f116cc
commit
ec14173256
@ -1,10 +1,21 @@
|
|||||||
import { API } from "$lib/config/api";
|
import { API } from "$lib/config/api";
|
||||||
|
import { Contact } from "@lucide/svelte";
|
||||||
import EraserIcon from "@lucide/svelte/icons/eraser";
|
import EraserIcon from "@lucide/svelte/icons/eraser";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings";
|
||||||
|
|
||||||
export const contactSchema = z.object({
|
export const contactSchema = z.object({
|
||||||
NameFirst: z.string().min(1, "Required"),
|
NameFirst: z.string().min(1, "Required"),
|
||||||
Initial: 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 = {
|
export const contactInitialForm = {
|
||||||
@ -24,8 +35,8 @@ export const contactInitialForm = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const contactDetailInitialForm = {
|
export const contactDetailInitialForm = {
|
||||||
SiteID: '',
|
|
||||||
ContactDetID: '',
|
ContactDetID: '',
|
||||||
|
SiteID: '',
|
||||||
ContactCode: '',
|
ContactCode: '',
|
||||||
ContactEmail: '',
|
ContactEmail: '',
|
||||||
OccupationID: '',
|
OccupationID: '',
|
||||||
@ -36,6 +47,15 @@ export const contactDetailInitialForm = {
|
|||||||
export const contactDefaultErrors = {
|
export const contactDefaultErrors = {
|
||||||
NameFirst: "Required",
|
NameFirst: "Required",
|
||||||
Initial: "Required",
|
Initial: "Required",
|
||||||
|
EmailAddress1: null,
|
||||||
|
EmailAddress2: null,
|
||||||
|
MobilePhone1: null,
|
||||||
|
MobilePhone2: null,
|
||||||
|
Phone: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const contactDetailDefaultErrors = {
|
||||||
|
ContactEmail: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const contactFormFields = [
|
export const contactFormFields = [
|
||||||
@ -102,13 +122,15 @@ export const contactFormFields = [
|
|||||||
key: "EmailAddress1",
|
key: "EmailAddress1",
|
||||||
label: "Email Address 1",
|
label: "Email Address 1",
|
||||||
required: false,
|
required: false,
|
||||||
type: "text",
|
type: "email",
|
||||||
|
validateOn: ["input"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "EmailAddress2",
|
key: "EmailAddress2",
|
||||||
label: "Email Address 2",
|
label: "Email Address 2",
|
||||||
required: false,
|
required: false,
|
||||||
type: "text",
|
type: "email",
|
||||||
|
validateOn: ["input"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -120,18 +142,21 @@ export const contactFormFields = [
|
|||||||
label: "Mobile Phone 1",
|
label: "Mobile Phone 1",
|
||||||
required: false,
|
required: false,
|
||||||
type: "text",
|
type: "text",
|
||||||
|
validateOn: ["input"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "MobilePhone2",
|
key: "MobilePhone2",
|
||||||
label: "Mobile Phone 2",
|
label: "Mobile Phone 2",
|
||||||
required: false,
|
required: false,
|
||||||
type: "text",
|
type: "text",
|
||||||
|
validateOn: ["input"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Phone",
|
key: "Phone",
|
||||||
label: "Phone",
|
label: "Phone",
|
||||||
required: false,
|
required: false,
|
||||||
type: "text",
|
type: "text",
|
||||||
|
validateOn: ["input"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -163,6 +188,7 @@ export const contactFormFields = [
|
|||||||
|
|
||||||
export const contactDetailFormFields = [
|
export const contactDetailFormFields = [
|
||||||
{
|
{
|
||||||
|
title: "Contact Detail Information",
|
||||||
rows: [
|
rows: [
|
||||||
{
|
{
|
||||||
type: "row",
|
type: "row",
|
||||||
@ -222,3 +248,22 @@ export function getContactFormActions(handlers) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@ -4,6 +4,14 @@
|
|||||||
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte";
|
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
|
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();
|
let props = $props();
|
||||||
|
|
||||||
@ -11,11 +19,23 @@
|
|||||||
|
|
||||||
const { formState } = masterDetail;
|
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 helpers = useDictionaryForm(formState);
|
||||||
|
|
||||||
const handlers = {
|
const handlers = {
|
||||||
clearForm: () => {
|
clearForm: () => {
|
||||||
formState.reset();
|
formState.reset();
|
||||||
|
contactDetailFormState.reset();
|
||||||
|
tempDetailContact = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,11 +43,80 @@
|
|||||||
|
|
||||||
let showConfirm = $state(false);
|
let showConfirm = $state(false);
|
||||||
|
|
||||||
async function handleSave() {
|
function snapshotForm() {
|
||||||
const result = await formState.save(masterDetail.mode);
|
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!');
|
function resetContactDetailForm() {
|
||||||
masterDetail?.exitForm(true);
|
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({
|
const primaryAction = $derived({
|
||||||
@ -46,6 +135,83 @@
|
|||||||
formFields={formFields}
|
formFields={formFields}
|
||||||
mode="create"
|
mode="create"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Separator class="my-4"/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<DictionaryFormRenderer
|
||||||
|
formState={contactDetailFormState}
|
||||||
|
formFields={contactDetailFormFields}
|
||||||
|
mode="create"
|
||||||
|
/>
|
||||||
|
<div class="flex gap-2 mt-1 ms-2">
|
||||||
|
{#if editingId !== null}
|
||||||
|
<Button size="sm" class="cursor-pointer" onclick={handleUpdateDetail}>Update</Button>
|
||||||
|
<Button size="sm" variant="outline" class="cursor-pointer" onclick={handleCancelEditDetail}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button size="sm" class="cursor-pointer" onclick={handleInsertDetail}>Insert</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<Separator />
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row class="hover:bg-transparent">
|
||||||
|
<Table.Head>Site</Table.Head>
|
||||||
|
<Table.Head>Code</Table.Head>
|
||||||
|
<Table.Head>Department</Table.Head>
|
||||||
|
<Table.Head>Occupation</Table.Head>
|
||||||
|
<Table.Head>Job Title</Table.Head>
|
||||||
|
<Table.Head>Email</Table.Head>
|
||||||
|
<Table.Head class="w-[80px]"></Table.Head>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{#if tempDetailContact.length === 0}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colspan={9} class="text-center text-muted-foreground py-6">
|
||||||
|
No data. Fill the form above and click Insert.
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{:else}
|
||||||
|
{#each tempDetailContact as row (row.id)}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell>{row.SiteID}</Table.Cell>
|
||||||
|
<Table.Cell>{row.ContactCode}</Table.Cell>
|
||||||
|
<Table.Cell>{row.Department}</Table.Cell>
|
||||||
|
<Table.Cell>{row.OccupationID}</Table.Cell>
|
||||||
|
<Table.Cell>{row.JobTitle}</Table.Cell>
|
||||||
|
<Table.Cell>{row.ContactEmail}</Table.Cell>
|
||||||
|
<Table.Cell class="w-[80px]">
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-7 w-7 cursor-pointer"
|
||||||
|
onclick={() => handleEditDetail(row)}
|
||||||
|
>
|
||||||
|
<PencilIcon class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-7 w-7 cursor-pointer"
|
||||||
|
onclick={() => handleRemoveDetail(row.id)}
|
||||||
|
>
|
||||||
|
<Trash2Icon class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
</FormPageContainer>
|
</FormPageContainer>
|
||||||
|
|
||||||
<ReusableAlertDialog
|
<ReusableAlertDialog
|
||||||
|
|||||||
309
src/lib/components/dictionary/contact/page/edit-page copy.svelte
Normal file
309
src/lib/components/dictionary/contact/page/edit-page copy.svelte
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
<script>
|
||||||
|
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--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormPageContainer title="Edit Contact" {primaryAction} {secondaryActions}>
|
||||||
|
<DictionaryFormRenderer
|
||||||
|
{formState}
|
||||||
|
formFields={formFields}
|
||||||
|
mode="edit"
|
||||||
|
/>
|
||||||
|
<Separator class="my-4"/>
|
||||||
|
<div class="flex flex-col px-2 py-1 gap-2 h-fit w-full">
|
||||||
|
<TopbarWrapper
|
||||||
|
title="Contact Detail"
|
||||||
|
actions={actionsDetail}
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
{#if showDetailForm}
|
||||||
|
<Card.Root class="w-full gap-2 2xl:gap-4 py-2 2xl:py-4">
|
||||||
|
<Card.Content class="space-y-3">
|
||||||
|
<DictionaryFormRenderer
|
||||||
|
formState={detailFormState}
|
||||||
|
formFields={contactDetailFormFields}
|
||||||
|
/>
|
||||||
|
</Card.Content>
|
||||||
|
<Card.Footer class="flex justify-end flex-end gap-2">
|
||||||
|
<Button size="sm" onclick={saveDetail}>
|
||||||
|
{isEditingDetail ? 'Update Detail' : 'Save Detail'}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onclick={cancelDetail}>Cancel</Button>
|
||||||
|
</Card.Footer>
|
||||||
|
</Card.Root>
|
||||||
|
{/if}
|
||||||
|
{#each masterDetail.selectedItem?.data?.Details as contactdetail, index}
|
||||||
|
<Card.Root class="w-full gap-2 2xl:gap-4 py-2 2xl:py-4">
|
||||||
|
<Card.Header>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Card.Title class="text-sm font-medium">
|
||||||
|
{contactdetail.ContactCode || "null"}
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description class="text-sm font-medium">
|
||||||
|
{contactdetail.ContactEmail || "null"}
|
||||||
|
</Card.Description>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<Badge variant="outline" class="text-xs">
|
||||||
|
{getLabel('SiteID', contactdetail.SiteID)}
|
||||||
|
</Badge>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost" class="size-7"
|
||||||
|
onclick={() => editDetail(index)}
|
||||||
|
>
|
||||||
|
<Edit2Icon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost" class="size-7"
|
||||||
|
onclick={() => removeDetail(index)}
|
||||||
|
>
|
||||||
|
<XIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="space-y-3">
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Department
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium">
|
||||||
|
{getLabel('Department', contactdetail.Department)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Job Title
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium">
|
||||||
|
{contactdetail.JobTitle || "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
Occupation
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium">
|
||||||
|
<!-- {contactdetail.OccupationID || "-"} -->
|
||||||
|
{getLabel('OccupationID', contactdetail.OccupationID)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormPageContainer>
|
||||||
|
|
||||||
|
<ReusableAlertDialog
|
||||||
|
bind:open={masterDetail.showExitConfirm}
|
||||||
|
onConfirm={masterDetail.confirmExit}
|
||||||
|
/>
|
||||||
@ -16,6 +16,7 @@
|
|||||||
import { useForm } from "$lib/components/composable/use-form.svelte";
|
import { useForm } from "$lib/components/composable/use-form.svelte";
|
||||||
import XIcon from "@lucide/svelte/icons/x";
|
import XIcon from "@lucide/svelte/icons/x";
|
||||||
import Edit2Icon from "@lucide/svelte/icons/edit-2";
|
import Edit2Icon from "@lucide/svelte/icons/edit-2";
|
||||||
|
import { getChangedFields } from "$lib/utils/getChangedFields";
|
||||||
|
|
||||||
let props = $props();
|
let props = $props();
|
||||||
|
|
||||||
@ -43,6 +44,10 @@
|
|||||||
|
|
||||||
let showConfirm = $state(false);
|
let showConfirm = $state(false);
|
||||||
|
|
||||||
|
let editingId = $state(null);
|
||||||
|
let idCounter = $state(0);
|
||||||
|
let tempMap = $state([]);
|
||||||
|
|
||||||
function getLabel(fieldKey, value) {
|
function getLabel(fieldKey, value) {
|
||||||
if (!detailFormState.selectOptions?.[fieldKey]) return value;
|
if (!detailFormState.selectOptions?.[fieldKey]) return value;
|
||||||
const option = detailFormState.selectOptions[fieldKey].find(opt => opt.value === value);
|
const option = detailFormState.selectOptions[fieldKey].find(opt => opt.value === value);
|
||||||
@ -83,21 +88,49 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let editedDetails = $state(masterDetail.selectedItem?.data?.Details || []);
|
||||||
|
|
||||||
async function handleEdit() {
|
async function handleEdit() {
|
||||||
const customPayload = {
|
const originalDetails = masterDetail.selectedItem?.data?.Details || [];
|
||||||
...formState.form,
|
|
||||||
Details: masterDetail.selectedItem?.data?.Details || []
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await formState.save(masterDetail.mode, customPayload);
|
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') {
|
// const customPayload = {
|
||||||
console.log('Contact updated successfully');
|
// ...formState.form,
|
||||||
toast('Contact Updated!');
|
// Details: masterDetail.selectedItem?.data?.Details || []
|
||||||
masterDetail.exitForm(true);
|
// };
|
||||||
} else {
|
// console.log('Custom Payload for Edit:', JSON.stringify(customPayload));
|
||||||
console.error('Failed to update contact:', result.message);
|
// 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({
|
const primaryAction = $derived({
|
||||||
|
|||||||
@ -125,10 +125,10 @@
|
|||||||
tempMap,
|
tempMap,
|
||||||
});
|
});
|
||||||
console.log(payload)
|
console.log(payload)
|
||||||
const result = await formState.save(masterDetail.mode, payload);
|
// const result = await formState.save(masterDetail.mode, payload);
|
||||||
|
|
||||||
toast('Test Map Created!');
|
// toast('Test Map Created!');
|
||||||
masterDetail?.exitForm(true);
|
// masterDetail?.exitForm(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const primaryAction = $derived({
|
const primaryAction = $derived({
|
||||||
@ -223,7 +223,6 @@
|
|||||||
// formState.form.ClientTestName = 'nyaho';
|
// formState.form.ClientTestName = 'nyaho';
|
||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
$inspect(mapFormFieldsTransformed)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FormPageContainer title="Create Test Map" {primaryAction} {secondaryActions} {actions}>
|
<FormPageContainer title="Create Test Map" {primaryAction} {secondaryActions} {actions}>
|
||||||
|
|||||||
@ -103,7 +103,7 @@ export function viewActions(handlers){
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
Icon: PencilIcon,
|
Icon: PencilIcon,
|
||||||
label: 'Edit Patient',
|
label: 'Edit Visit',
|
||||||
onClick: handlers.editPatient,
|
onClick: handlers.editPatient,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
import { API } from "$lib/config/api";
|
import { API } from "$lib/config/api";
|
||||||
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
|
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
|
||||||
import { buildPatientPayload } from "$lib/components/patient/list/config/patient-form-config";
|
import { buildPatientPayload } from "$lib/components/patient/list/config/patient-form-config";
|
||||||
|
import { getChangedFields } from "$lib/utils/getChangedFields";
|
||||||
|
|
||||||
let props = $props();
|
let props = $props();
|
||||||
|
|
||||||
@ -61,16 +62,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() {
|
async function handleEdit() {
|
||||||
const currentPayload = buildPatientPayload(formState.form);
|
const currentPayload = buildPatientPayload(formState.form);
|
||||||
const originalPayload = buildPatientPayload(masterDetail.formSnapshot);
|
const originalPayload = buildPatientPayload(masterDetail.formSnapshot);
|
||||||
|
|||||||
9
src/lib/utils/getChangedFields.js
Normal file
9
src/lib/utils/getChangedFields.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user