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) {
console.log(cleanEmptyStrings(formData));
try {
const res = await fetch(`${API.BASE_URL}${endpoint}`, {
method: 'POST',

View File

@ -11,15 +11,13 @@ export function useForm({schema, initialForm, defaultErrors = {}, mode = 'create
state.isSaving.current = true
try {
// const payload = { ...state.form };
const payload = customPayload || { ...state.form };
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') {
const id = payload[idKey];
result = await editEndpoint(payload, id);
const { [idKey]: _, ...body } = payload;
result = await editEndpoint(body, id);
} else {
result = await saveEndpoint(payload);
}

View File

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

View File

@ -5,6 +5,10 @@ import { z } from "zod";
export const accountSchema = z.object({
Initial: 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 = {
@ -12,13 +16,13 @@ export const accountInitialForm = {
ParentAccount: '',
AccountName: '',
Initial: '',
Street_1: '',
Street_2: '',
Street_3: '',
Country: '',
Province: '',
City: '',
ZIP: '',
Street_1: '',
Street_2: '',
Street_3: '',
EmailAddress1: '',
EmailAddress2: '',
Phone: '',
@ -28,6 +32,10 @@ export const accountInitialForm = {
export const accountDefaultErrors = {
Initial: "Required",
AccountName: "Required",
EmailAddress1: null,
EmailAddress2: null,
ZIP: null,
Phone: null,
};
export const accountFormFields = [
@ -162,6 +170,7 @@ export const accountFormFields = [
label: "Phone",
required: false,
type: "text",
validateOn: ["input"]
},
]
},

View File

@ -13,7 +13,6 @@ export const searchFields = [
export const detailSections = [
{
title: "",
class: "grid grid-cols-1 md:grid-cols-2 gap-4",
groups: [
{
@ -35,7 +34,8 @@ export const detailSections = [
{ key: "MobilePhone1", label: "Mobile Phone 1" },
{ key: "MobilePhone2", label: "Mobile Phone 2" },
]
}
},
]
},
{
@ -45,6 +45,12 @@ export const detailSections = [
{ 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) {

View File

@ -232,7 +232,13 @@ export const contactDetailFormFields = [
type: "row",
columns: [
{ 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 = {
...mainForm,
Details: tempDetailContact.map((item) => ({
ContactDetID: item.ContactDetID,
SiteID: item.SiteID,
ContactCode: item.ContactCode,
ContactEmail: item.ContactEmail,

View File

@ -11,12 +11,15 @@
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 { contactDetailSchema, contactDetailInitialForm, contactDetailDefaultErrors, contactDetailFormFields, buildContactPayload } 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";
// 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();
@ -24,33 +27,24 @@
const { formState } = masterDetail;
const detailFormState = useForm({
schema: null,
const contactDetailFormState = useForm({
schema: contactDetailSchema,
initialForm: contactDetailInitialForm,
defaultErrors: {},
mode: 'create',
modeOpt: 'default',
saveEndpoint: null,
editEndpoint: null,
defaultErrors: contactDetailDefaultErrors,
});
let showDetailForm = $state(false);
let editingDetailIndex = $state(null);
let isEditingDetail = $derived(editingDetailIndex !== null);
const helpers = useDictionaryForm(formState);
let showConfirm = $state(false);
let editingId = $state(null);
let idCounter = $state(0);
let tempMap = $state([]);
let tempDetailContact = $state([]);
let deletedDetailIds = $state([]);
function getLabel(fieldKey, value) {
if (!detailFormState.selectOptions?.[fieldKey]) return value;
const option = detailFormState.selectOptions[fieldKey].find(opt => opt.value === value);
if (!contactDetailFormState.selectOptions?.[fieldKey]) return value;
const option = contactDetailFormState.selectOptions[fieldKey].find(opt => opt.value === value);
return option?.label || value || "-";
}
@ -80,7 +74,7 @@
group.rows.forEach(row => {
row.columns.forEach(col => {
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() {
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 });
const originalPayload = $state.snapshot({ ...masterDetail.formSnapshot, Details: originalDetails });
console.log('Current Payload:', editedDetails);
console.log('Original Payload:', originalDetails);
console.log(finalPayload);
// console.log('Original Payload:', JSON.stringify(originalPayload));
// console.log('Current Payload:', JSON.stringify(currentPayload));
// console.log('Changed Fields:', getChangedFields(originalPayload, currentPayload));
// console.log(originalPayload.Details);
// console.log(currentPayload.Details);
// console.log('Diff:', diffDetails(currentPayload.Details, originalPayload.Details));
// const originalDetails = masterDetail.selectedItem?.data?.Details || [];
// const currentPayload = $state.snapshot({ ...formState.form, Details: editedDetails });
// const originalPayload = $state.snapshot({ ...masterDetail.formSnapshot, Details: originalDetails });
// console.log('Current Payload:', editedDetails);
// console.log('Original Payload:', originalDetails);
// const customPayload = {
// ...formState.form,
// Details: masterDetail.selectedItem?.data?.Details || []
// };
// console.log('Custom Payload for Edit:', JSON.stringify(customPayload));
// const result = await formState.save(masterDetail.mode, customPayload);
// // const 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));
// // ***
// const changedFields = getChangedFields(originalPayload, currentPayload);
// console.log('Changed Fields:', JSON.stringify(changedFields));
// if (Object.keys(changedFields).length === 0) {
// toast('No changes detected');
// return;
// }
// // if (Object.keys(changedFields).length === 0) {
// // toast('No changes detected');
// // return;
// // }
// const payload = {
// ContactID: formState.form.ContactID,
// ...changedFields
// };
// // const payload = {
// // ContactID: formState.form.ContactID,
// // ...changedFields
// // };
// console.log('Custom Payload for Edit:', payload);
// ***
// // console.log('Custom Payload for Edit:', payload);
// // ***
// const result = await formState.save(masterDetail.mode, 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);
// }
// // 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({
@ -142,71 +203,114 @@
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;
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 ?? "",
};
});
}
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!');
function resetContactDetailForm() {
contactDetailFormState.reset();
editingId = null;
}
function handleInsertDetail() {
const row = {
id: ++idCounter,
ContactDetID: null,
...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 ?
// {
// ...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);
}
// 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--;
tempDetailContact = tempDetailContact.filter((row) => row.id !== id);
if (editingId === id) {
resetContactDetailForm();
}
}
$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>
<FormPageContainer title="Edit Contact" {primaryAction} {secondaryActions}>
@ -215,96 +319,82 @@
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>
<DictionaryFormRenderer
formState={contactDetailFormState}
formFields={contactDetailFormFields}
mode="create"
/>
<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>
<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}
{#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)}
</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)}
>
<Edit2Icon class="h-4 w-4" />
<PencilIcon class="h-3.5 w-3.5" />
</Button>
<Button
size="icon"
variant="ghost" class="size-7"
onclick={() => removeDetail(index)}
<Button
size="icon"
variant="ghost"
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>
</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>
</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body>
</Table.Root>
</div>
</FormPageContainer>

View File

@ -8,6 +8,7 @@
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 * as Table from "$lib/components/ui/table/index.js";
let props = $props();
@ -41,6 +42,46 @@
}
</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 })}
<div class="space-y-1.5">
<dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
@ -62,7 +103,7 @@
title={masterDetail.selectedItem.data.NameFirst}
{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}
<div class="p-4">
{#if section.groups}
@ -84,11 +125,26 @@
{:else}
<div class={section.class}>
{#each section.fields as field}
{@render Fieldset({
label: field.label,
value: getFieldValue(field),
isUTCDate: field.isUTCDate
})}
{#if field.fullWidth}
<div class="col-span-2">
{#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}
</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}
</div>
{/if}
@ -96,62 +152,6 @@
{/each}
</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}
<ReusableEmpty icon={UserXIcon} desc="Select a contact to see details"/>
{/if}

View File

@ -13,6 +13,6 @@ export async function createContainer(newContainerForm) {
return await create(API.CONTAINER, newContainerForm)
}
export async function editContainer(editContainerForm) {
return await update(API.CONTAINER, editContainerForm)
export async function editContainer(editContainerForm, id) {
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",
fields: [
{ key: "ConClass", label: "Container Class" },
{ key: "ConClassLabel", label: "Container Class" },
{ key: "Color", label: "Color" },
{ key: "Additive", label: "Additive" },
{ key: "AdditiveLabel", label: "Additive" },
]
},
];

View File

@ -6,6 +6,7 @@
import { untrack } from "svelte";
import { API } from "$lib/config/api";
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
import { getChangedFields } from "$lib/utils/getChangedFields";
let props = $props();
@ -38,7 +39,24 @@
});
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') {
console.log('Container updated successfully');
@ -46,6 +64,8 @@
masterDetail.exitForm(true);
} else {
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)
}
export async function editLocation(editLocationForm) {
return await update(API.LOCATION, editLocationForm)
export async function editLocation(editLocationForm, id) {
return await update(API.LOCATION, editLocationForm, id)
}

View File

@ -43,7 +43,8 @@ export const detailSections = [
fields: [
{ key: "Province", label: "Province" },
{ 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: [
{ key: "Street1", label: "Street 1" },
{ 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"),
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"),
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 = {
LocationID: '',
LocCode: '',
LocType: '',
LocFull: '',
SiteID: '',
LocCode: '',
Parent: '',
LocFull: '',
Description: '',
LocType: '',
Street1: '',
Street2: '',
Phone: '',
Email: '',
City: '',
Province: '',
ZIP: '',
City: '',
PostCode: '',
GeoLocationSystem: '',
GeoLocationData: '',
Phone: '',
Mobile: '',
Email: '',
};
export const locationDefaultErrors = {
@ -32,7 +36,8 @@ export const locationDefaultErrors = {
LocFull: "Required",
Email: null,
Phone: null,
ZIP: null,
Mobile: null,
PostCode: null,
};
export const locationFormFields = [
@ -52,17 +57,26 @@ export const locationFormFields = [
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`,
},
{
key: "LocCode",
label: "Location Code",
required: true,
type: "text",
validateOn: ["input"]
key: "Parent",
label: "Parent",
required: false,
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
valueKey: "SiteID",
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`,
},
]
},
{
type: "row",
columns: [
{
key: "LocCode",
label: "Location Code",
required: true,
type: "text",
validateOn: ["input"]
},
{
key: "LocType",
label: "Location Type",
@ -70,13 +84,25 @@ export const locationFormFields = [
type: "select",
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/location_type`,
},
]
},
{
type: "row",
columns: [
{
key: "LocFull",
label: "Location Name",
required: true,
type: "text",
validateOn: ["input"]
}
},
{
key: "Description",
label: "Description",
required: false,
type: "text",
},
]
},
]
@ -126,8 +152,8 @@ export const locationFormFields = [
type: "row",
columns: [
{
key: "ZIP",
label: "ZIP",
key: "PostCode",
label: "Post Code",
required: false,
type: "text",
validateOn: ["input"],
@ -150,13 +176,20 @@ export const locationFormFields = [
type: "text",
validateOn: ["input"]
},
{
key: "Mobile",
label: "Mobile",
required: false,
type: "text",
validateOn: ["input"]
},
{
key: "Email",
label: "Email",
required: false,
type: "text",
validateOn: ["input"]
}
},
]
},
]

View File

@ -6,6 +6,7 @@
import { untrack } from "svelte";
import { API } from "$lib/config/api";
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
import { getChangedFields } from "$lib/utils/getChangedFields";
let props = $props();
@ -50,7 +51,24 @@
});
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') {
console.log('Location updated successfully');
@ -58,6 +76,8 @@
masterDetail.exitForm(true);
} else {
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)
}
export async function editOccupation(editOccupationForm) {
return await update(API.OCCUPATION, editOccupationForm)
export async function editOccupation(editOccupationForm, id) {
return await update(API.OCCUPATION, editOccupationForm, id)
}

View File

@ -6,6 +6,7 @@
import { untrack } from "svelte";
import { API } from "$lib/config/api";
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
import { getChangedFields } from "$lib/utils/getChangedFields";
let props = $props();
@ -38,7 +39,24 @@
});
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') {
console.log('Occupation updated successfully');
@ -46,6 +64,8 @@
masterDetail.exitForm(true);
} else {
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: "HostID", label: "Host 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',
VISITLIST: '/api/patvisit/patient/',
COUNTER: '/api/counter',
CONTAINER: '/api/specimen/containerdef',
CONTAINER: '/api/specimen/container',
PROVINCE: '/api/areageo/provinces',
CITY: '/api/areageo/cities',
ACCOUNT: '/api/organization/account',

View File

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

View File

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

View File

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

View File

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