continue contact edit & edit function

This commit is contained in:
faiztyanirh 2026-04-07 17:43:21 +07:00
parent ec14173256
commit 6afc0067e2
23 changed files with 512 additions and 300 deletions

View File

@ -76,6 +76,7 @@ export async function searchWithPath(endpoint, searchQuery) {
} }
export async function create(endpoint, formData) { export async function create(endpoint, formData) {
console.log(cleanEmptyStrings(formData));
try { try {
const res = await fetch(`${API.BASE_URL}${endpoint}`, { const res = await fetch(`${API.BASE_URL}${endpoint}`, {
method: 'POST', method: 'POST',

View File

@ -11,15 +11,13 @@ export function useForm({schema, initialForm, defaultErrors = {}, mode = 'create
state.isSaving.current = true state.isSaving.current = true
try { try {
// const payload = { ...state.form };
const payload = customPayload || { ...state.form }; const payload = customPayload || { ...state.form };
let result; let result;
// const { ProvinceID, CityID, ...rest } = state.form;
// const payload = customPayload || rest;
// const result = currentMode === 'edit' ? await editEndpoint(payload, idKey) : await saveEndpoint(payload);
if (currentMode === 'edit') { if (currentMode === 'edit') {
const id = payload[idKey]; const id = payload[idKey];
result = await editEndpoint(payload, id); const { [idKey]: _, ...body } = payload;
result = await editEndpoint(body, id);
} else { } else {
result = await saveEndpoint(payload); result = await saveEndpoint(payload);
} }

View File

@ -10,6 +10,7 @@ export async function getAccount(searchQuery) {
} }
export async function createAccount(newAccountForm) { export async function createAccount(newAccountForm) {
console.log(newAccountForm);
return await create(API.ACCOUNT, newAccountForm) return await create(API.ACCOUNT, newAccountForm)
} }

View File

@ -5,6 +5,10 @@ import { z } from "zod";
export const accountSchema = z.object({ export const accountSchema = z.object({
Initial: z.string().min(1, "Required"), Initial: z.string().min(1, "Required"),
AccountName: z.string().min(1, "Required"), AccountName: 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"),
ZIP: z.string().regex(/^$|^[0-9]+$/, "Can only contain numbers"),
Phone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"),
}); });
export const accountInitialForm = { export const accountInitialForm = {
@ -12,13 +16,13 @@ export const accountInitialForm = {
ParentAccount: '', ParentAccount: '',
AccountName: '', AccountName: '',
Initial: '', Initial: '',
Street_1: '',
Street_2: '',
Street_3: '',
Country: '', Country: '',
Province: '', Province: '',
City: '', City: '',
ZIP: '', ZIP: '',
Street_1: '',
Street_2: '',
Street_3: '',
EmailAddress1: '', EmailAddress1: '',
EmailAddress2: '', EmailAddress2: '',
Phone: '', Phone: '',
@ -28,6 +32,10 @@ export const accountInitialForm = {
export const accountDefaultErrors = { export const accountDefaultErrors = {
Initial: "Required", Initial: "Required",
AccountName: "Required", AccountName: "Required",
EmailAddress1: null,
EmailAddress2: null,
ZIP: null,
Phone: null,
}; };
export const accountFormFields = [ export const accountFormFields = [
@ -162,6 +170,7 @@ export const accountFormFields = [
label: "Phone", label: "Phone",
required: false, required: false,
type: "text", type: "text",
validateOn: ["input"]
}, },
] ]
}, },

View File

@ -13,7 +13,6 @@ export const searchFields = [
export const detailSections = [ export const detailSections = [
{ {
title: "",
class: "grid grid-cols-1 md:grid-cols-2 gap-4", class: "grid grid-cols-1 md:grid-cols-2 gap-4",
groups: [ groups: [
{ {
@ -35,7 +34,8 @@ export const detailSections = [
{ key: "MobilePhone1", label: "Mobile Phone 1" }, { key: "MobilePhone1", label: "Mobile Phone 1" },
{ key: "MobilePhone2", label: "Mobile Phone 2" }, { key: "MobilePhone2", label: "Mobile Phone 2" },
] ]
} },
] ]
}, },
{ {
@ -45,6 +45,12 @@ export const detailSections = [
{ key: "SubSpecialty", label: "Sub Specialty" }, { key: "SubSpecialty", label: "Sub Specialty" },
] ]
}, },
{
class: "grid grid-cols-2 gap-4 items-center",
fields: [
{ key: "Details", label: "Details", fullWidth: true },
]
}
]; ];
export function contactActions(masterDetail) { export function contactActions(masterDetail) {

View File

@ -232,7 +232,13 @@ export const contactDetailFormFields = [
type: "row", type: "row",
columns: [ columns: [
{ key: "JobTitle", label: "Job Title", required: false, type: "text" }, { key: "JobTitle", label: "Job Title", required: false, type: "text" },
{ key: "ContactEmail", label: "Email", required: false, type: "text" }, {
key: "ContactEmail",
label: "Email",
required: false,
type: "text",
validateOn: ["input"],
},
] ]
} }
] ]
@ -256,6 +262,7 @@ export function buildContactPayload({
let payload = { let payload = {
...mainForm, ...mainForm,
Details: tempDetailContact.map((item) => ({ Details: tempDetailContact.map((item) => ({
ContactDetID: item.ContactDetID,
SiteID: item.SiteID, SiteID: item.SiteID,
ContactCode: item.ContactCode, ContactCode: item.ContactCode,
ContactEmail: item.ContactEmail, ContactEmail: item.ContactEmail,

View File

@ -11,12 +11,15 @@
import * as Card from "$lib/components/ui/card/index.js"; import * as Card from "$lib/components/ui/card/index.js";
import { Badge } from "$lib/components/ui/badge/index.js"; import { Badge } from "$lib/components/ui/badge/index.js";
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte"; import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
import { contactDetailInitialForm, contactDetailFormFields } from "$lib/components/dictionary/contact/config/contact-form-config"; import { contactDetailSchema, contactDetailInitialForm, contactDetailDefaultErrors, contactDetailFormFields, buildContactPayload } from "$lib/components/dictionary/contact/config/contact-form-config";
import { Button } from "$lib/components/ui/button/index.js"; import { Button } from "$lib/components/ui/button/index.js";
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"; // import { getChangedFields } from "$lib/utils/getChangedFields";
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";
let props = $props(); let props = $props();
@ -24,33 +27,24 @@
const { formState } = masterDetail; const { formState } = masterDetail;
const detailFormState = useForm({ const contactDetailFormState = useForm({
schema: null, schema: contactDetailSchema,
initialForm: contactDetailInitialForm, initialForm: contactDetailInitialForm,
defaultErrors: {}, defaultErrors: contactDetailDefaultErrors,
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); const helpers = useDictionaryForm(formState);
let showConfirm = $state(false); let showConfirm = $state(false);
let editingId = $state(null); let editingId = $state(null);
let idCounter = $state(0); let idCounter = $state(0);
let tempMap = $state([]); let tempDetailContact = $state([]);
let deletedDetailIds = $state([]);
function getLabel(fieldKey, value) { function getLabel(fieldKey, value) {
if (!detailFormState.selectOptions?.[fieldKey]) return value; if (!contactDetailFormState.selectOptions?.[fieldKey]) return value;
const option = detailFormState.selectOptions[fieldKey].find(opt => opt.value === value); const option = contactDetailFormState.selectOptions[fieldKey].find(opt => opt.value === value);
return option?.label || value || "-"; return option?.label || value || "-";
} }
@ -80,7 +74,7 @@
group.rows.forEach(row => { group.rows.forEach(row => {
row.columns.forEach(col => { row.columns.forEach(col => {
if (col.type === "select" && col.optionsEndpoint) { if (col.type === "select" && col.optionsEndpoint) {
detailFormState.fetchOptions(col, detailFormState.form); contactDetailFormState.fetchOptions(col, contactDetailFormState.form);
} }
}); });
}); });
@ -88,49 +82,116 @@
}); });
}); });
let editedDetails = $state(masterDetail.selectedItem?.data?.Details || []); // 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;
// console.log('ITEM:', item);
// console.log('ORIG:', orig);
// console.log('KEYS current:', Object.keys(item));
// console.log('KEYS original:', Object.keys(orig));
const changed = Object.keys(item).some(
key => item[key] !== orig[key]
);
if (changed) updated.push(item);
}
return updated;
}
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 originalDetails = masterDetail.selectedItem?.data?.Details || []; 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
}))
}
};
const currentPayload = $state.snapshot({ ...formState.form, Details: editedDetails }); console.log(finalPayload);
const originalPayload = $state.snapshot({ ...masterDetail.formSnapshot, Details: originalDetails }); // console.log('Original Payload:', JSON.stringify(originalPayload));
console.log('Current Payload:', editedDetails); // console.log('Current Payload:', JSON.stringify(currentPayload));
console.log('Original Payload:', originalDetails); // console.log('Changed Fields:', getChangedFields(originalPayload, currentPayload));
// console.log(originalPayload.Details);
// console.log(currentPayload.Details);
// console.log('Diff:', diffDetails(currentPayload.Details, originalPayload.Details));
// const customPayload = { // const originalDetails = masterDetail.selectedItem?.data?.Details || [];
// ...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 currentPayload = $state.snapshot({ ...formState.form, Details: editedDetails });
const changedFields = getChangedFields(originalPayload, currentPayload); // const originalPayload = $state.snapshot({ ...masterDetail.formSnapshot, Details: originalDetails });
console.log('Changed Fields:', JSON.stringify(changedFields)); // console.log('Current Payload:', editedDetails);
// console.log('Original Payload:', originalDetails);
// if (Object.keys(changedFields).length === 0) { // // const customPayload = {
// toast('No changes detected'); // // ...formState.form,
// return; // // Details: masterDetail.selectedItem?.data?.Details || []
// } // // };
// // console.log('Custom Payload for Edit:', JSON.stringify(customPayload));
// // const result = await formState.save(masterDetail.mode, customPayload);
// const payload = { // // ***
// ContactID: formState.form.ContactID, // const changedFields = getChangedFields(originalPayload, currentPayload);
// ...changedFields // console.log('Changed Fields:', JSON.stringify(changedFields));
// };
// console.log('Custom Payload for Edit:', payload); // // if (Object.keys(changedFields).length === 0) {
// *** // // toast('No changes detected');
// // return;
// // }
// const result = await formState.save(masterDetail.mode, payload); // // const payload = {
// // ContactID: formState.form.ContactID,
// // ...changedFields
// // };
// if (result.status === 'success') { // // console.log('Custom Payload for Edit:', payload);
// console.log('Contact updated successfully'); // // ***
// toast('Contact Updated!');
// masterDetail.exitForm(true); // // const result = await formState.save(masterDetail.mode, payload);
// } else {
// console.error('Failed to update contact:', result.message); // // 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({
@ -142,71 +203,114 @@
const secondaryActions = []; const secondaryActions = [];
const actionsDetail = [ function snapshotForm() {
{ return untrack(() => {
Icon: PlusIcon, const f = contactDetailFormState.form;
label: 'Add Contact Detail', return {
onClick: () => addDetail(), SiteID: f.SiteID ?? "",
}, ContactCode: f.ContactCode ?? "",
]; ContactEmail: f.ContactEmail ?? "",
Department: f.Department ?? "",
function addDetail() { OccupationID: f.OccupationID ?? "",
editingDetailIndex = null; // Mode create baru JobTitle: f.JobTitle ?? "",
detailFormState.reset(); // Reset form ke initialForm };
detailFormState.setForm({ ...contactDetailInitialForm }); // Set form kosong });
showDetailForm = true;
} }
async function saveDetail() { function resetContactDetailForm() {
// Ambil current form dari detailFormState.form contactDetailFormState.reset();
const newDetail = { ...detailFormState.form }; editingId = null;
}
if (isEditingDetail) { function handleInsertDetail() {
// Mode edit: update detail yang ada const row = {
masterDetail.selectedItem.data.Details[editingDetailIndex] = newDetail; id: ++idCounter,
toast('Contact Detail Updated!'); ContactDetID: null,
} else { ...snapshotForm()
// Mode create: tambah detail baru };
if (!masterDetail.selectedItem.data.Details) {
masterDetail.selectedItem.data.Details = []; tempDetailContact = [...tempDetailContact, row];
}
masterDetail.selectedItem.data.Details.push(newDetail); resetContactDetailForm();
toast('Contact Detail Added!'); }
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 ?
// {
// ...row,
// ...snapshotForm()
// } : row
// );
// resetContactDetailForm();
// }
function handleUpdateDetail() {
const updated = snapshotForm();
tempDetailContact = tempDetailContact.map((row) =>
row.id === editingId
?
{
...row,
...updated,
ContactDetID: row.ContactDetID ?? null
} : row
);
resetContactDetailForm();
}
function handleCancelEditDetail() {
resetContactDetailForm();
}
$inspect(deletedDetailIds)
function handleRemoveDetail(id) {
const row = tempDetailContact.find(r => r.id === id);
if (row?.ContactDetID) {
deletedDetailIds.push(row.ContactDetID);
} }
tempDetailContact = tempDetailContact.filter((row) => row.id !== id);
// Reset form dan tutup form if (editingId === id) {
detailFormState.reset(); resetContactDetailForm();
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--;
} }
} }
$effect(() => {
const mainForm = formState.form;
if (mainForm.Details && Array.isArray(mainForm.Details)) {
tempDetailContact = mainForm.Details.map((row, index) => ({
id: row.id ?? index + 1,
...row,
}));
}
})
$effect(() => {
const maxId = tempDetailContact.reduce((max, row) => {
const rowId = typeof row.id === 'number' ? row.id : 0;
return rowId > max ? rowId : max;
}, 0);
if (maxId > idCounter) {
idCounter = maxId;
}
});
</script> </script>
<FormPageContainer title="Edit Contact" {primaryAction} {secondaryActions}> <FormPageContainer title="Edit Contact" {primaryAction} {secondaryActions}>
@ -215,96 +319,82 @@
formFields={formFields} formFields={formFields}
mode="edit" mode="edit"
/> />
<Separator class="my-4"/> <Separator class="my-4"/>
<div class="flex flex-col px-2 py-1 gap-2 h-fit w-full">
<TopbarWrapper <div>
title="Contact Detail" <DictionaryFormRenderer
actions={actionsDetail} formState={contactDetailFormState}
formFields={contactDetailFormFields}
mode="create"
/> />
<div class="flex flex-col gap-4"> <div class="flex gap-2 mt-1 ms-2">
{#if showDetailForm} {#if editingId !== null}
<Card.Root class="w-full gap-2 2xl:gap-4 py-2 2xl:py-4"> <Button size="sm" class="cursor-pointer" onclick={handleUpdateDetail}>Update</Button>
<Card.Content class="space-y-3"> <Button size="sm" variant="outline" class="cursor-pointer" onclick={handleCancelEditDetail}>
<DictionaryFormRenderer Cancel
formState={detailFormState} </Button>
formFields={contactDetailFormFields} {:else}
/> <Button size="sm" class="cursor-pointer" onclick={handleInsertDetail}>Insert</Button>
</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} {/if}
{#each masterDetail.selectedItem?.data?.Details as contactdetail, index} </div>
<Card.Root class="w-full gap-2 2xl:gap-4 py-2 2xl:py-4"> </div>
<Card.Header>
<div class="flex items-start justify-between"> <div class="mt-4">
<div class="space-y-1"> <Separator />
<Card.Title class="text-sm font-medium"> <Table.Root>
{contactdetail.ContactCode || "null"} <Table.Header>
</Card.Title> <Table.Row class="hover:bg-transparent">
<Card.Description class="text-sm font-medium"> <Table.Head>Site</Table.Head>
{contactdetail.ContactEmail || "null"} <Table.Head>Code</Table.Head>
</Card.Description> <Table.Head>Department</Table.Head>
</div> <Table.Head>Occupation</Table.Head>
<div class="flex items-center gap-4"> <Table.Head>Job Title</Table.Head>
<Badge variant="outline" class="text-xs"> <Table.Head>Email</Table.Head>
{getLabel('SiteID', contactdetail.SiteID)} <Table.Head class="w-[80px]"></Table.Head>
</Badge> </Table.Row>
<div> </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 <Button
size="icon" size="icon"
variant="ghost" class="size-7" variant="ghost"
onclick={() => editDetail(index)} class="h-7 w-7 cursor-pointer"
onclick={() => handleEditDetail(row)}
> >
<Edit2Icon class="h-4 w-4" /> <PencilIcon class="h-3.5 w-3.5" />
</Button> </Button>
<Button <Button
size="icon" size="icon"
variant="ghost" class="size-7" variant="ghost"
onclick={() => removeDetail(index)} class="h-7 w-7 cursor-pointer"
onclick={() => handleRemoveDetail(row.id)}
> >
<XIcon class="h-4 w-4" /> <Trash2Icon class="h-3.5 w-3.5" />
</Button> </Button>
</div> </div>
</div> </Table.Cell>
</div> </Table.Row>
</Card.Header> {/each}
<Card.Content class="space-y-3"> {/if}
<div class="grid grid-cols-3 gap-3"> </Table.Body>
<div class="space-y-1"> </Table.Root>
<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> </div>
</FormPageContainer> </FormPageContainer>

View File

@ -8,6 +8,7 @@
import PlusIcon from "@lucide/svelte/icons/plus"; import PlusIcon from "@lucide/svelte/icons/plus";
import * as Card from "$lib/components/ui/card/index.js"; import * as Card from "$lib/components/ui/card/index.js";
import { Badge } from "$lib/components/ui/badge/index.js"; import { Badge } from "$lib/components/ui/badge/index.js";
import * as Table from "$lib/components/ui/table/index.js";
let props = $props(); let props = $props();
@ -41,6 +42,46 @@
} }
</script> </script>
{#snippet DetailsTable({ value, label })}
<div class="space-y-1.5 w-full">
<dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{label}
</dt>
<dd>
{#if value && Array.isArray(value) && value.length > 0}
<div class="border rounded-md">
<Table.Root>
<Table.Header>
<Table.Row>
<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.Row>
</Table.Header>
<Table.Body>
{#each value as row, i}
<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.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
{:else}
<span class="text-sm font-medium">-</span>
{/if}
</dd>
</div>
{/snippet}
{#snippet Fieldset({ value, label, isUTCDate = false })} {#snippet Fieldset({ value, label, isUTCDate = false })}
<div class="space-y-1.5"> <div class="space-y-1.5">
<dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider"> <dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
@ -62,7 +103,7 @@
title={masterDetail.selectedItem.data.NameFirst} title={masterDetail.selectedItem.data.NameFirst}
{actions} {actions}
/> />
<div class="flex-1 min-h-0 overflow-y-auto space-y-4"> <div class="flex-1 min-h-0 overflow-y-auto">
{#each detailSections as section} {#each detailSections as section}
<div class="p-4"> <div class="p-4">
{#if section.groups} {#if section.groups}
@ -84,11 +125,26 @@
{:else} {:else}
<div class={section.class}> <div class={section.class}>
{#each section.fields as field} {#each section.fields as field}
{@render Fieldset({ {#if field.fullWidth}
label: field.label, <div class="col-span-2">
value: getFieldValue(field), {#if field.key === "Details"}
isUTCDate: field.isUTCDate {@render DetailsTable({ label: field.label, value: getFieldValue(field) })}
})} {:else}
{@render Fieldset({ label: field.label, value: getFieldValue(field), isUTCDate: field.isUTCDate })}
{/if}
</div>
{:else if field.key === "Details"}
{@render DetailsTable({
label: field.label,
value: getFieldValue(field),
})}
{:else}
{@render Fieldset({
label: field.label,
value: getFieldValue(field),
isUTCDate: field.isUTCDate
})}
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}
@ -96,62 +152,6 @@
{/each} {/each}
</div> </div>
</div> </div>
<Separator />
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
<TopbarWrapper
title="Contact Detail"
actions={actionsDetail}
/>
<div class="flex flex-col gap-4">
{#each masterDetail.selectedItem?.data?.Details as contactdetail}
<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}
</Card.Title>
<Card.Description class="text-sm font-medium">
{contactdetail.ContactEmail}
</Card.Description>
</div>
<Badge variant="secondary" class="text-xs">
Site {contactdetail.SiteID}
</Badge>
</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">
{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 || "-"}
</p>
</div>
</div>
</Card.Content>
</Card.Root>
{/each}
</div>
</div>
{:else} {:else}
<ReusableEmpty icon={UserXIcon} desc="Select a contact to see details"/> <ReusableEmpty icon={UserXIcon} desc="Select a contact to see details"/>
{/if} {/if}

View File

@ -13,6 +13,6 @@ export async function createContainer(newContainerForm) {
return await create(API.CONTAINER, newContainerForm) return await create(API.CONTAINER, newContainerForm)
} }
export async function editContainer(editContainerForm) { export async function editContainer(editContainerForm, id) {
return await update(API.CONTAINER, editContainerForm) return await update(API.CONTAINER, editContainerForm, id)
} }

View File

@ -28,9 +28,9 @@ export const detailSections = [
{ {
class: "grid grid-cols-2 gap-4 items-center", class: "grid grid-cols-2 gap-4 items-center",
fields: [ fields: [
{ key: "ConClass", label: "Container Class" }, { key: "ConClassLabel", label: "Container Class" },
{ key: "Color", label: "Color" }, { key: "Color", label: "Color" },
{ key: "Additive", label: "Additive" }, { key: "AdditiveLabel", label: "Additive" },
] ]
}, },
]; ];

View File

@ -6,6 +6,7 @@
import { untrack } from "svelte"; import { untrack } from "svelte";
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 { getChangedFields } from "$lib/utils/getChangedFields";
let props = $props(); let props = $props();
@ -38,7 +39,24 @@
}); });
async function handleEdit() { async function handleEdit() {
const result = await formState.save(masterDetail.mode); const currentPayload = formState.form;
const originalPayload = masterDetail.formSnapshot;
const changedFields = getChangedFields(originalPayload, currentPayload);
if (Object.keys(changedFields).length === 0) {
toast('No changes detected');
return;
}
const payload = {
ConDefID: formState.form.ConDefID,
...changedFields
};
console.log('Payload:', payload);
const result = await formState.save(masterDetail.mode, payload);
if (result.status === 'success') { if (result.status === 'success') {
console.log('Container updated successfully'); console.log('Container updated successfully');
@ -46,6 +64,8 @@
masterDetail.exitForm(true); masterDetail.exitForm(true);
} else { } else {
console.error('Failed to update container:', result.message); console.error('Failed to update container:', result.message);
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to update container';
toast.error(errorMessages)
} }
} }

View File

@ -13,6 +13,6 @@ export async function createLocation(newLocationForm) {
return await create(API.LOCATION, newLocationForm) return await create(API.LOCATION, newLocationForm)
} }
export async function editLocation(editLocationForm) { export async function editLocation(editLocationForm, id) {
return await update(API.LOCATION, editLocationForm) return await update(API.LOCATION, editLocationForm, id)
} }

View File

@ -43,7 +43,8 @@ export const detailSections = [
fields: [ fields: [
{ key: "Province", label: "Province" }, { key: "Province", label: "Province" },
{ key: "City", label: "City" }, { key: "City", label: "City" },
{ key: "PostCode", label: "ZIP" }, { key: "PostCode", label: "Post Code" },
{ key: "Email", label: "Email" },
] ]
}, },
{ {
@ -51,8 +52,10 @@ export const detailSections = [
fields: [ fields: [
{ key: "Street1", label: "Street 1" }, { key: "Street1", label: "Street 1" },
{ key: "Street2", label: "Street 2" }, { key: "Street2", label: "Street 2" },
{ key: "Phone", label: "Phone" },
{ key: "Mobile", label: "Mobile" },
] ]
} },
] ]
}, },
{ {

View File

@ -7,24 +7,28 @@ export const locationSchema = z.object({
LocFull: z.string().min(1, "Required"), LocFull: z.string().min(1, "Required"),
Email: z.string().trim().optional().refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),"Invalid email format"), Email: z.string().trim().optional().refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),"Invalid email format"),
Phone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"), Phone: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"),
ZIP: z.string().regex(/^$|^[0-9]+$/, "Can only contain numbers"), Mobile: z.string().max(14, "Max 14 chars").regex(/^$|^[0-9]+$/, "Can only contain numbers"),
PostCode: z.string().regex(/^$|^[0-9]+$/, "Can only contain numbers"),
}); });
export const locationInitialForm = { export const locationInitialForm = {
LocationID: '', LocationID: '',
LocCode: '',
LocType: '',
LocFull: '',
SiteID: '', SiteID: '',
LocCode: '',
Parent: '',
LocFull: '',
Description: '',
LocType: '',
Street1: '', Street1: '',
Street2: '', Street2: '',
Phone: '',
Email: '',
City: '',
Province: '', Province: '',
ZIP: '', City: '',
PostCode: '',
GeoLocationSystem: '', GeoLocationSystem: '',
GeoLocationData: '', GeoLocationData: '',
Phone: '',
Mobile: '',
Email: '',
}; };
export const locationDefaultErrors = { export const locationDefaultErrors = {
@ -32,7 +36,8 @@ export const locationDefaultErrors = {
LocFull: "Required", LocFull: "Required",
Email: null, Email: null,
Phone: null, Phone: null,
ZIP: null, Mobile: null,
PostCode: null,
}; };
export const locationFormFields = [ export const locationFormFields = [
@ -52,17 +57,26 @@ export const locationFormFields = [
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`, labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`,
}, },
{ {
key: "LocCode", key: "Parent",
label: "Location Code", label: "Parent",
required: true, required: false,
type: "text", type: "select",
validateOn: ["input"] optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
valueKey: "SiteID",
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`,
}, },
] ]
}, },
{ {
type: "row", type: "row",
columns: [ columns: [
{
key: "LocCode",
label: "Location Code",
required: true,
type: "text",
validateOn: ["input"]
},
{ {
key: "LocType", key: "LocType",
label: "Location Type", label: "Location Type",
@ -70,13 +84,25 @@ export const locationFormFields = [
type: "select", type: "select",
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/location_type`, optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/location_type`,
}, },
]
},
{
type: "row",
columns: [
{ {
key: "LocFull", key: "LocFull",
label: "Location Name", label: "Location Name",
required: true, required: true,
type: "text", type: "text",
validateOn: ["input"] validateOn: ["input"]
} },
{
key: "Description",
label: "Description",
required: false,
type: "text",
},
] ]
}, },
] ]
@ -126,8 +152,8 @@ export const locationFormFields = [
type: "row", type: "row",
columns: [ columns: [
{ {
key: "ZIP", key: "PostCode",
label: "ZIP", label: "Post Code",
required: false, required: false,
type: "text", type: "text",
validateOn: ["input"], validateOn: ["input"],
@ -150,13 +176,20 @@ export const locationFormFields = [
type: "text", type: "text",
validateOn: ["input"] validateOn: ["input"]
}, },
{
key: "Mobile",
label: "Mobile",
required: false,
type: "text",
validateOn: ["input"]
},
{ {
key: "Email", key: "Email",
label: "Email", label: "Email",
required: false, required: false,
type: "text", type: "text",
validateOn: ["input"] validateOn: ["input"]
} },
] ]
}, },
] ]

View File

@ -6,6 +6,7 @@
import { untrack } from "svelte"; import { untrack } from "svelte";
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 { getChangedFields } from "$lib/utils/getChangedFields";
let props = $props(); let props = $props();
@ -50,7 +51,24 @@
}); });
async function handleEdit() { async function handleEdit() {
const result = await formState.save(masterDetail.mode); const currentPayload = formState.form;
const originalPayload = masterDetail.formSnapshot;
const changedFields = getChangedFields(originalPayload, currentPayload);
if (Object.keys(changedFields).length === 0) {
toast('No changes detected');
return;
}
const payload = {
LocationID: formState.form.LocationID,
...changedFields
};
console.log('Payload:', payload);
const result = await formState.save(masterDetail.mode, payload);
if (result.status === 'success') { if (result.status === 'success') {
console.log('Location updated successfully'); console.log('Location updated successfully');
@ -58,6 +76,8 @@
masterDetail.exitForm(true); masterDetail.exitForm(true);
} else { } else {
console.error('Failed to update location:', result.message); console.error('Failed to update location:', result.message);
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to update location';
toast.error(errorMessages)
} }
} }

View File

@ -13,6 +13,6 @@ export async function createOccupation(newOccupationForm) {
return await create(API.OCCUPATION, newOccupationForm) return await create(API.OCCUPATION, newOccupationForm)
} }
export async function editOccupation(editOccupationForm) { export async function editOccupation(editOccupationForm, id) {
return await update(API.OCCUPATION, editOccupationForm) return await update(API.OCCUPATION, editOccupationForm, id)
} }

View File

@ -6,6 +6,7 @@
import { untrack } from "svelte"; import { untrack } from "svelte";
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 { getChangedFields } from "$lib/utils/getChangedFields";
let props = $props(); let props = $props();
@ -38,7 +39,24 @@
}); });
async function handleEdit() { async function handleEdit() {
const result = await formState.save(masterDetail.mode); const currentPayload = formState.form;
const originalPayload = masterDetail.formSnapshot;
const changedFields = getChangedFields(originalPayload, currentPayload);
if (Object.keys(changedFields).length === 0) {
toast('No changes detected');
return;
}
const payload = {
OccupationID: formState.form.OccupationID,
...changedFields
};
console.log('Payload:', payload);
const result = await formState.save(masterDetail.mode, payload);
if (result.status === 'success') { if (result.status === 'success') {
console.log('Occupation updated successfully'); console.log('Occupation updated successfully');
@ -46,6 +64,8 @@
masterDetail.exitForm(true); masterDetail.exitForm(true);
} else { } else {
console.error('Failed to update occupation:', result.message); console.error('Failed to update occupation:', result.message);
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to update occupation';
toast.error(errorMessages)
} }
} }

View File

@ -30,7 +30,7 @@ export const detailSections = [
{ key: "ClientTypeLabel", label: "Client Type" }, { key: "ClientTypeLabel", label: "Client Type" },
{ key: "HostID", label: "Host ID" }, { key: "HostID", label: "Host ID" },
{ key: "ClientID", label: "Client ID" }, { key: "ClientID", label: "Client ID" },
{ key: "details", label: "Details", fullWidth: true }, { key: "Details", label: "Details", fullWidth: true },
] ]
}, },
]; ];

View File

@ -16,7 +16,7 @@ export const API = {
PATVISIT: '/api/patvisit', PATVISIT: '/api/patvisit',
VISITLIST: '/api/patvisit/patient/', VISITLIST: '/api/patvisit/patient/',
COUNTER: '/api/counter', COUNTER: '/api/counter',
CONTAINER: '/api/specimen/containerdef', CONTAINER: '/api/specimen/container',
PROVINCE: '/api/areageo/provinces', PROVINCE: '/api/areageo/provinces',
CITY: '/api/areageo/cities', CITY: '/api/areageo/cities',
ACCOUNT: '/api/organization/account', ACCOUNT: '/api/organization/account',

View File

@ -20,6 +20,7 @@
modeOpt: 'cascade', modeOpt: 'cascade',
saveEndpoint: createAccount, saveEndpoint: createAccount,
editEndpoint: editAccount, editEndpoint: editAccount,
idKey: 'AccountID',
} }
}); });

View File

@ -20,6 +20,7 @@
modeOpt: 'default', modeOpt: 'default',
saveEndpoint: createContainer, saveEndpoint: createContainer,
editEndpoint: editContainer, editEndpoint: editContainer,
idKey: 'ConDefID',
} }
}); });

View File

@ -20,6 +20,7 @@
modeOpt: 'cascade', modeOpt: 'cascade',
saveEndpoint: createLocation, saveEndpoint: createLocation,
editEndpoint: editLocation, editEndpoint: editLocation,
idKey: 'LocationID',
} }
}); });

View File

@ -20,6 +20,7 @@
modeOpt: 'default', modeOpt: 'default',
saveEndpoint: createOccupation, saveEndpoint: createOccupation,
editEndpoint: editOccupation, editEndpoint: editOccupation,
idKey: 'OccupationID',
} }
}); });