mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-23 01:29:27 +07:00
add feature and bug fix
patient list : menambahkan client validation patientid boleh '.' dan '-' fix isDeadLabel karena backend gamau ngeluarin valuenya contact : tambahkan guard pada handlesave fix create contact gak nampilin detail di payload fix edit contact updated ga nampil occupation : testing spinner untuk indikasi rowclick testing close popover setelah save selesai
This commit is contained in:
parent
ffb57539e8
commit
239147f7ec
@ -17,7 +17,7 @@ export function useSearch(searchFields, searchApiFunction) {
|
||||
return query;
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
async function handleSearch(onDone) {
|
||||
isLoading = true;
|
||||
try {
|
||||
searchData = await searchApiFunction(searchQuery);
|
||||
@ -25,6 +25,7 @@ export function useSearch(searchFields, searchApiFunction) {
|
||||
console.error('Search failed:', error);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
onDone?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
|
||||
import Settings2Icon from "@lucide/svelte/icons/settings-2";
|
||||
import PencilIcon from "@lucide/svelte/icons/pencil";
|
||||
import { API } from "$lib/config/api";
|
||||
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
|
||||
|
||||
export const searchFields = [
|
||||
{
|
||||
@ -53,8 +54,13 @@ export const detailSections = [
|
||||
}
|
||||
];
|
||||
|
||||
export function contactActions(masterDetail) {
|
||||
export function contactActions(masterDetail, handlers) {
|
||||
return [
|
||||
{
|
||||
Icon: RefreshIcon,
|
||||
label: 'Refresh Data',
|
||||
onClick: handlers.refresh,
|
||||
},
|
||||
{
|
||||
Icon: PlusIcon,
|
||||
label: 'Add Contact',
|
||||
|
||||
@ -113,10 +113,16 @@
|
||||
tempDetailContact,
|
||||
});
|
||||
console.log(payload)
|
||||
const result = await formState.save(masterDetail.mode);
|
||||
const result = await formState.save(masterDetail.mode, payload);
|
||||
|
||||
toast('Contact Created!');
|
||||
masterDetail?.exitForm(true);
|
||||
if (result.status === 'success') {
|
||||
toast('Contact Created!');
|
||||
masterDetail?.exitForm(true);
|
||||
} else {
|
||||
console.error('Failed to save contact');
|
||||
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to save contact';
|
||||
toast.error(errorMessages)
|
||||
}
|
||||
}
|
||||
|
||||
const primaryAction = $derived({
|
||||
|
||||
@ -1,309 +0,0 @@
|
||||
<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}
|
||||
/>
|
||||
@ -21,6 +21,11 @@
|
||||
|
||||
const { formState } = masterDetail;
|
||||
|
||||
let editingId = $state(null);
|
||||
let idCounter = $state(0);
|
||||
let tempDetailContact = $state([]);
|
||||
let deletedDetailIds = $state([]);
|
||||
|
||||
const contactDetailFormState = useForm({
|
||||
schema: contactDetailSchema,
|
||||
initialForm: contactDetailInitialForm,
|
||||
@ -31,158 +36,7 @@
|
||||
|
||||
let showConfirm = $state(false);
|
||||
|
||||
let editingId = $state(null);
|
||||
let idCounter = $state(0);
|
||||
let tempDetailContact = $state([]);
|
||||
let deletedDetailIds = $state([]);
|
||||
|
||||
function getLabel(fieldKey, value) {
|
||||
if (!contactDetailFormState.selectOptions?.[fieldKey]) return value;
|
||||
const option = contactDetailFormState.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) {
|
||||
contactDetailFormState.fetchOptions(col, contactDetailFormState.form);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
const changed = Object.keys(item).some(
|
||||
key => item[key] !== orig[key]
|
||||
);
|
||||
|
||||
if (changed) updated.push(item);
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
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
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
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 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 = [];
|
||||
|
||||
function snapshotForm() {
|
||||
function snapshotForm() {
|
||||
return untrack(() => {
|
||||
const f = contactDetailFormState.form;
|
||||
return {
|
||||
@ -228,17 +82,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// function handleUpdateDetail() {
|
||||
// tempDetailContact = tempDetailContact.map((row) =>
|
||||
// row.id === editingId ?
|
||||
// {
|
||||
// ...row,
|
||||
// ...snapshotForm()
|
||||
// } : row
|
||||
// );
|
||||
// resetContactDetailForm();
|
||||
// }
|
||||
|
||||
function handleUpdateDetail() {
|
||||
const updated = snapshotForm();
|
||||
|
||||
@ -270,6 +113,144 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getLabel(fieldKey, value) {
|
||||
if (!contactDetailFormState.selectOptions?.[fieldKey]) return value;
|
||||
const option = contactDetailFormState.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 === "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) {
|
||||
contactDetailFormState.fetchOptions(col, contactDetailFormState.form);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
// const changed = Object.keys(item).some(
|
||||
// key => item[key] !== orig[key]
|
||||
// );
|
||||
|
||||
// if (changed) updated.push(item);
|
||||
// }
|
||||
|
||||
// return updated;
|
||||
// }
|
||||
|
||||
function diffDetails(currentRows, originalRows) {
|
||||
const originalMap = new Map(
|
||||
originalRows
|
||||
.filter(item => item.ContactDetID)
|
||||
.map(item => [item.ContactDetID, item])
|
||||
);
|
||||
|
||||
const updated = [];
|
||||
const detailKeys = ['SiteID', 'ContactCode', 'ContactEmail', 'OccupationID', 'JobTitle', 'Department'];
|
||||
|
||||
for (const item of currentRows) {
|
||||
if (!item.ContactDetID) continue;
|
||||
|
||||
const orig = originalMap.get(item.ContactDetID);
|
||||
if (!orig) continue;
|
||||
|
||||
const changed = detailKeys.some(
|
||||
key => item[key] !== orig[key]
|
||||
);
|
||||
|
||||
if (changed) updated.push(item);
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
const currentPayload = buildContactPayload({
|
||||
mainForm: formState.form,
|
||||
tempDetailContact
|
||||
});
|
||||
const originalPayload = buildContactPayload({
|
||||
mainForm: masterDetail.formSnapshot,
|
||||
tempDetailContact: masterDetail.formSnapshot.Details ?? []
|
||||
});
|
||||
const originalRows = masterDetail.formSnapshot.Details ?? [];
|
||||
const updatedDetails = diffDetails(tempDetailContact, originalRows);
|
||||
|
||||
const changedFields = getChangedFields(originalPayload, currentPayload);
|
||||
const hasMainChanges = Object.keys(changedFields).length > 0;
|
||||
const hasDetailChanges = updatedDetails.length > 0 || tempDetailContact.some(r => !r.ContactDetID) || deletedDetailIds.length > 0;
|
||||
|
||||
if (!hasMainChanges && !hasDetailChanges) {
|
||||
toast('No changes detected');
|
||||
return;
|
||||
}
|
||||
|
||||
const finalPayload = {
|
||||
ContactID: formState.form.ContactID,
|
||||
...changedFields,
|
||||
...(hasDetailChanges && {
|
||||
Details: {
|
||||
created: tempDetailContact.filter(r => !r.ContactDetID),
|
||||
edited: updatedDetails,
|
||||
deleted: deletedDetailIds
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
console.log(finalPayload);
|
||||
|
||||
const result = await formState.save(masterDetail.mode, finalPayload);
|
||||
|
||||
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 errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to update contact';
|
||||
toast.error(errorMessages)
|
||||
}
|
||||
}
|
||||
|
||||
const primaryAction = $derived({
|
||||
label: 'Edit',
|
||||
onClick: handleEdit,
|
||||
disabled: helpers.hasErrors || formState.isSaving.current,
|
||||
loading: formState.isSaving.current
|
||||
});
|
||||
|
||||
const secondaryActions = [];
|
||||
|
||||
$effect(() => {
|
||||
const mainForm = formState.form;
|
||||
if (mainForm.Details && Array.isArray(mainForm.Details)) {
|
||||
|
||||
@ -8,12 +8,15 @@
|
||||
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
|
||||
import UserXIcon from "@lucide/svelte/icons/user-x";
|
||||
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
|
||||
|
||||
let props = $props();
|
||||
|
||||
const search = useSearch(searchFields, getContacts);
|
||||
const initialForm = props.masterDetail.formState.form;
|
||||
const actions = contactActions(props.masterDetail, initialForm)
|
||||
const handlers = {
|
||||
refresh: () => {search.handleSearch()},
|
||||
};
|
||||
const actions = contactActions(props.masterDetail, handlers)
|
||||
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
|
||||
|
||||
let activeRowId = $state(null);
|
||||
@ -38,10 +41,13 @@
|
||||
>
|
||||
<div class={`flex w-full ${props.masterDetail.isFormMode ? "flex-col justify-center h-full items-center" : "flex-col justify-start h-full"}`} >
|
||||
{#if props.masterDetail.isFormMode}
|
||||
<span class="flex flex-col items-center justify-center gap-4 tracking-widest font-semibold select-none">
|
||||
{#each "CONTACT".split("") as c}
|
||||
<span class="leading-none">{c}</span>
|
||||
{/each}
|
||||
<span class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
|
||||
<MoveLeftIcon />
|
||||
<div class="flex flex-col items-center justify-center flex-grow gap-4">
|
||||
{#each "CONTACT".split("") as c}
|
||||
<span class="leading-none">{c}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import PlusIcon from "@lucide/svelte/icons/plus";
|
||||
import Settings2Icon from "@lucide/svelte/icons/settings-2";
|
||||
import PencilIcon from "@lucide/svelte/icons/pencil";
|
||||
import { API } from "$lib/config/api";
|
||||
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
|
||||
|
||||
export const searchFields = [
|
||||
{
|
||||
@ -67,8 +68,13 @@ export const detailSections = [
|
||||
},
|
||||
];
|
||||
|
||||
export function locationActions(masterDetail) {
|
||||
export function locationActions(masterDetail, handlers) {
|
||||
return [
|
||||
{
|
||||
Icon: RefreshIcon,
|
||||
label: 'Refresh Data',
|
||||
onClick: handlers.refresh,
|
||||
},
|
||||
{
|
||||
Icon: PlusIcon,
|
||||
label: 'Add Location',
|
||||
|
||||
@ -8,12 +8,15 @@
|
||||
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
|
||||
import MapPinXIcon from "@lucide/svelte/icons/map-pin-x";
|
||||
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
|
||||
|
||||
let props = $props();
|
||||
|
||||
const search = useSearch(searchFields, getLocations);
|
||||
const initialForm = props.masterDetail.formState.form;
|
||||
const actions = locationActions(props.masterDetail, initialForm)
|
||||
const handlers = {
|
||||
refresh: () => {search.handleSearch()},
|
||||
};
|
||||
const actions = locationActions(props.masterDetail, handlers)
|
||||
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
|
||||
|
||||
let activeRowId = $state(null);
|
||||
@ -38,10 +41,13 @@
|
||||
>
|
||||
<div class={`flex w-full ${props.masterDetail.isFormMode ? "flex-col justify-center h-full items-center" : "flex-col justify-start h-full"}`} >
|
||||
{#if props.masterDetail.isFormMode}
|
||||
<span class="flex flex-col items-center justify-center gap-4 tracking-widest font-semibold select-none">
|
||||
{#each "LOCATION".split("") as c}
|
||||
<span class="leading-none">{c}</span>
|
||||
{/each}
|
||||
<span class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
|
||||
<MoveLeftIcon />
|
||||
<div class="flex flex-col items-center justify-center flex-grow gap-4">
|
||||
{#each "LOCATION".split("") as c}
|
||||
<span class="leading-none">{c}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||
import Settings2Icon from "@lucide/svelte/icons/settings-2";
|
||||
import PencilIcon from "@lucide/svelte/icons/pencil";
|
||||
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
|
||||
|
||||
export const searchFields = [
|
||||
{
|
||||
@ -37,8 +38,13 @@ export const detailSections = [
|
||||
},
|
||||
];
|
||||
|
||||
export function occupationActions(masterDetail) {
|
||||
export function occupationActions(masterDetail, handlers) {
|
||||
return [
|
||||
{
|
||||
Icon: RefreshIcon,
|
||||
label: 'Refresh Data',
|
||||
onClick: handlers.refresh,
|
||||
},
|
||||
{
|
||||
Icon: PlusIcon,
|
||||
label: 'Add Location',
|
||||
|
||||
@ -8,20 +8,23 @@
|
||||
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
|
||||
import FolderXIcon from "@lucide/svelte/icons/folder-x";
|
||||
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
|
||||
|
||||
let props = $props();
|
||||
|
||||
const search = useSearch(searchFields, getOccupations);
|
||||
const initialForm = props.masterDetail.formState.form;
|
||||
const actions = occupationActions(props.masterDetail, initialForm)
|
||||
const handlers = {
|
||||
refresh: () => {search.handleSearch()},
|
||||
};
|
||||
const actions = occupationActions(props.masterDetail, handlers)
|
||||
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
|
||||
|
||||
let activeRowId = $state(null);
|
||||
</script>
|
||||
|
||||
{#snippet searchParamSnippet()}
|
||||
{#snippet searchParamSnippet(close)}
|
||||
<ReusableSearchParam {searchFields}
|
||||
bind:searchQuery={search.searchQuery} onSearch={search.handleSearch} onReset={search.handleReset} isLoading={search.isLoading}
|
||||
bind:searchQuery={search.searchQuery} onSearch={() => search.handleSearch(close)} onReset={search.handleReset} isLoading={search.isLoading}
|
||||
selectOptions={search.selectOptions} loadingOptions={search.loadingOptions} fetchOptions={search.fetchOptions}
|
||||
/>
|
||||
{/snippet}
|
||||
@ -38,10 +41,13 @@
|
||||
>
|
||||
<div class={`flex w-full ${props.masterDetail.isFormMode ? "flex-col justify-center h-full items-center" : "flex-col justify-start h-full"}`} >
|
||||
{#if props.masterDetail.isFormMode}
|
||||
<span class="flex flex-col items-center justify-center gap-4 tracking-widest font-semibold select-none">
|
||||
{#each "OCCUPATION".split("") as c}
|
||||
<span class="leading-none">{c}</span>
|
||||
{/each}
|
||||
<span class="flex flex-col items-center justify-start gap-4 tracking-widest font-semibold select-none h-full">
|
||||
<MoveLeftIcon />
|
||||
<div class="flex flex-col items-center justify-center flex-grow gap-4">
|
||||
{#each "OCCUPATION".split("") as c}
|
||||
<span class="leading-none">{c}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
|
||||
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||
import FolderXIcon from "@lucide/svelte/icons/folder-x";
|
||||
import { Spinner } from "$lib/components/ui/spinner/index.js";
|
||||
|
||||
let props = $props();
|
||||
|
||||
@ -46,7 +47,11 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#if masterDetail.selectedItem}
|
||||
{#if masterDetail.isLoadingDetail}
|
||||
<div class="h-full w-full flex items-center justify-center">
|
||||
<Spinner class="size-6" />
|
||||
</div>
|
||||
{:else if masterDetail.selectedItem}
|
||||
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
|
||||
<TopbarWrapper
|
||||
title={masterDetail.selectedItem.data.OccText}
|
||||
|
||||
@ -77,7 +77,7 @@ export const detailSections = [
|
||||
{ parentKey: "Custodian", key: "PatientID", label: "Custodian ID" },
|
||||
],
|
||||
},
|
||||
{ key: "isDeadLabel", label: "Deceased" },
|
||||
{ key: "isDead", label: "Deceased", valueMap: { 0: "N", 1: "Y" } },
|
||||
{ key: "CreateDate", label: "Create Date", isUTCDate: true },
|
||||
{ key: "DelDate", label: "Delete Date" },
|
||||
{ key: "TimeOfDeath", label: "Time of Death", isUTCDate: true },
|
||||
|
||||
@ -3,7 +3,7 @@ import EraserIcon from "@lucide/svelte/icons/eraser";
|
||||
import { z } from "zod";
|
||||
|
||||
export const patientSchema = z.object({
|
||||
PatientID: z.string().min(1, "Required"),
|
||||
PatientID: z.string().min(1, "Required").regex(/^[A-Za-z0-9.-]+$/, "Only letters, numbers, '-' and '.' are allowed"),
|
||||
Sex: z.string().min(1, "Required"),
|
||||
NameFirst: z.string().min(1, "Required"),
|
||||
Birthdate: z.string().min(1, "Required").refine(
|
||||
@ -56,7 +56,7 @@ export const patientInitialForm = {
|
||||
Phone: "",
|
||||
EmailAddress2: "",
|
||||
MobilePhone: "",
|
||||
isDead: "",
|
||||
isDead: 0,
|
||||
TimeOfDeath: "",
|
||||
LinkTo: [],
|
||||
PatCom: "",
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
showConfirm = true;
|
||||
}
|
||||
}
|
||||
// $inspect(formState.form)
|
||||
|
||||
function confirmDiscard() {
|
||||
masterDetail.exitForm(true);
|
||||
}
|
||||
@ -68,7 +68,6 @@
|
||||
disabled: formState.isSaving.current
|
||||
}
|
||||
];
|
||||
$inspect(formState.errors)
|
||||
</script>
|
||||
|
||||
<FormPageContainer title="Create Patient" {primaryAction} {secondaryActions} {actions}>
|
||||
|
||||
@ -42,8 +42,14 @@
|
||||
.filter(val => val && val.trim() !== "")
|
||||
.join(" / ");
|
||||
}
|
||||
let value = field.parentKey ? patient[field.parentKey]?.[field.key] : patient[field.key];
|
||||
|
||||
return field.parentKey ? patient[field.parentKey]?.[field.key] : patient[field.key];
|
||||
if (field.valueMap && value != null) {
|
||||
value = field.valueMap[value] ?? value;
|
||||
}
|
||||
|
||||
// return field.parentKey ? patient[field.parentKey]?.[field.key] : patient[field.key];
|
||||
return value;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
|
||||
let props = $props();
|
||||
const { Icon } = props;
|
||||
|
||||
let open = $state(false);
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider delayDuration={100}>
|
||||
@ -14,7 +16,7 @@
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props: tooltipProps })}
|
||||
{#if props.popoverContent}
|
||||
<Popover.Root>
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger>
|
||||
{#snippet child({ props: popoverProps })}
|
||||
<Button
|
||||
@ -28,7 +30,8 @@
|
||||
{/snippet}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content collisionPadding={props.collisionPadding ?? 0} class={props.popoverWidth ?? "w-72"}>
|
||||
{@render props.popoverContent()}
|
||||
<!-- {@render props.popoverContent()} -->
|
||||
{@render props.popoverContent(() => open = false)}
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
{:else}
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
modeOpt: 'default',
|
||||
saveEndpoint: createContact,
|
||||
editEndpoint: editContact,
|
||||
idKey: 'ContactID',
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user