faiztyanirh 239147f7ec 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
2026-04-13 18:06:07 +07:00

364 lines
12 KiB
Svelte

<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 { 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 { 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();
const { masterDetail, formFields, formActions, schema, initialForm } = props.context;
const { formState } = masterDetail;
let editingId = $state(null);
let idCounter = $state(0);
let tempDetailContact = $state([]);
let deletedDetailIds = $state([]);
const contactDetailFormState = useForm({
schema: contactDetailSchema,
initialForm: contactDetailInitialForm,
defaultErrors: contactDetailDefaultErrors,
});
const helpers = useDictionaryForm(formState);
let showConfirm = $state(false);
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 ?? "",
};
});
}
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() {
const updated = snapshotForm();
tempDetailContact = tempDetailContact.map((row) =>
row.id === editingId
?
{
...row,
...updated,
ContactDetID: row.ContactDetID ?? null
} : row
);
resetContactDetailForm();
}
function handleCancelEditDetail() {
resetContactDetailForm();
}
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);
if (editingId === id) {
resetContactDetailForm();
}
}
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)) {
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}>
<DictionaryFormRenderer
{formState}
formFields={formFields}
mode="edit"
/>
<Separator class="my-4"/>
<div>
<DictionaryFormRenderer
formState={contactDetailFormState}
formFields={contactDetailFormFields}
mode="create"
/>
<div class="flex gap-2 mt-1 ms-2">
{#if editingId !== null}
<Button size="sm" class="cursor-pointer" onclick={handleUpdateDetail}>Update</Button>
<Button size="sm" variant="outline" class="cursor-pointer" onclick={handleCancelEditDetail}>
Cancel
</Button>
{:else}
<Button size="sm" class="cursor-pointer" onclick={handleInsertDetail}>Insert</Button>
{/if}
</div>
</div>
<div class="mt-4">
<Separator />
<Table.Root>
<Table.Header>
<Table.Row class="hover:bg-transparent">
<Table.Head>Site</Table.Head>
<Table.Head>Code</Table.Head>
<Table.Head>Department</Table.Head>
<Table.Head>Occupation</Table.Head>
<Table.Head>Job Title</Table.Head>
<Table.Head>Email</Table.Head>
<Table.Head class="w-[80px]"></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if tempDetailContact.length === 0}
<Table.Row>
<Table.Cell colspan={9} class="text-center text-muted-foreground py-6">
No data. Fill the form above and click Insert.
</Table.Cell>
</Table.Row>
{:else}
{#each tempDetailContact as row (row.id)}
<Table.Row>
<Table.Cell>{row.SiteID}</Table.Cell>
<Table.Cell>{row.ContactCode}</Table.Cell>
<Table.Cell>{row.Department}</Table.Cell>
<Table.Cell>{row.OccupationID}</Table.Cell>
<Table.Cell>{row.JobTitle}</Table.Cell>
<Table.Cell>{row.ContactEmail}</Table.Cell>
<Table.Cell class="w-[80px]">
<div class="flex gap-1">
<Button
size="icon"
variant="ghost"
class="h-7 w-7 cursor-pointer"
onclick={() => handleEditDetail(row)}
>
<PencilIcon class="h-3.5 w-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
class="h-7 w-7 cursor-pointer"
onclick={() => handleRemoveDetail(row.id)}
>
<Trash2Icon class="h-3.5 w-3.5" />
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body>
</Table.Root>
</div>
</FormPageContainer>
<ReusableAlertDialog
bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit}
/>