506 lines
20 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 ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
import { useForm } from "$lib/components/composable/use-form.svelte";
import { testMapDetailSchema, testMapDetailInitialForm, testMapDetailDefaultErrors, testMapDetailFormFields, buildTestMapPayload } from "$lib/components/dictionary/testmap/config/testmap-form-config";
import { Separator } from "$lib/components/ui/separator/index.js";
import PencilIcon from "@lucide/svelte/icons/pencil";
import Trash2Icon from "@lucide/svelte/icons/trash-2";
import * as Table from '$lib/components/ui/table/index.js';
import { Button } from "$lib/components/ui/button/index.js";
import { untrack } from "svelte";
import { API } from "$lib/config/api";
import { getChangedFields } from "$lib/utils/getChangedFields";
import { onMount } from "svelte";
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
let props = $props();
const { masterDetail, formFields, formActions, schema, initialForm } = props.context;
const { formState } = masterDetail;
let editingId = $state(null);
let idCounter = $state(0);
let tempMap = $state([]);
let deletedDetailIds = $state([]);
let showDeleteConfirm = $state(false);
let selectedRow = $state({});
const testMapDetailFormState = useForm({
schema: testMapDetailSchema,
initialForm: testMapDetailInitialForm,
defaultErrors: testMapDetailDefaultErrors,
});
const helpers = useDictionaryForm(formState);
let showConfirm = $state(false);
function snapshotForm() {
return untrack(() => {
const f = testMapDetailFormState.form;
return {
HostTestCode: f.HostTestCode ?? "",
HostTestName: f.HostTestName ?? "",
ClientTestCode: f.ClientTestCode ?? "",
ClientTestName: f.ClientTestName ?? "",
ConDefID: f.ConDefID ?? "",
};
});
}
function resetTestMapDetailForm() {
testMapDetailFormState.reset();
editingId = null;
}
function handleInsertDetail() {
const row = {
id: ++idCounter,
TestMapDetailID: null,
...snapshotForm()
};
tempMap = [...tempMap, row];
resetTestMapDetailForm();
}
async function handleEditDetail(row) {
editingId = row.id;
untrack(() => {
const f = testMapDetailFormState.form;
f.HostTestCode = row.HostTestCode;
f.HostTestName = row.HostTestName;
f.ClientTestCode = row.ClientTestCode;
f.ClientTestName = row.ClientTestName;
f.ConDefID = row.ConDefID;
});
}
function handleUpdateDetail() {
const updated = snapshotForm();
tempMap = tempMap.map((row) =>
row.id === editingId ? {
...row,
...updated,
TestMapDetailID: row.TestMapDetailID ?? null
} : row
);
resetTestMapDetailForm();
}
function handleCancelEditDetail() {
resetTestMapDetailForm();
}
function handleRemoveDetail(id) {
const row = tempMap.find(r => r.id === id);
if (row?.TestMapDetailID) {
deletedDetailIds.push(row.TestMapDetailID);
}
tempMap = tempMap.filter((row) => row.id !== id);
if (editingId === id) {
resetTestMapDetailForm();
}
}
$effect(() => {
const fields = mapFormFieldsTransformed;
untrack(() => {
fields.forEach(group => {
group.rows.forEach(row => {
row.columns.forEach(col => {
if (col.type === 'select' && col.optionsEndpoint) {
formState.fetchOptions(col, formState.form);
}
});
});
});
});
});
$effect(() => {
const fields = mapDetailFormFieldsTransformed;
untrack(() => {
fields.forEach(group => {
group.rows.forEach(row => {
row.columns.forEach(col => {
if (col.type === 'select' && col.optionsEndpoint) {
testMapDetailFormState.fetchOptions(col, testMapDetailFormState.form);
}
});
});
});
});
});
function diffDetails(currentRows, originalRows) {
const originalMap = new Map(
originalRows
.filter(item => item.TestMapDetailID)
.map(item => [item.TestMapDetailID, item])
);
const updated = [];
const detailKeys = ['HostTestCode', 'HostTestName', 'ClientTestCode', 'ClientTestName', 'ConDefID'];
for (const item of currentRows) {
if (!item.TestMapDetailID) continue;
const orig = originalMap.get(item.TestMapDetailID);
if (!orig) continue;
const changed = detailKeys.some(
key => item[key] !== orig[key]
);
if (changed) updated.push(item);
}
return updated;
}
async function handleEdit() {
const currentPayload = buildTestMapPayload({
mainForm: formState.form,
tempMap
});
const originalPayload = buildTestMapPayload({
mainForm: masterDetail.formSnapshot,
tempMap: masterDetail.formSnapshot.details ?? []
});
const originalRows = masterDetail.formSnapshot.details ?? [];
const updatedDetails = diffDetails(tempMap, originalRows);
const changedFields = getChangedFields(originalPayload, currentPayload);
const hasMainChanges = Object.keys(changedFields).length > 0;
const hasDetailChanges = updatedDetails.length > 0 || tempMap.some(r => !r.TestMapDetailID) || deletedDetailIds.length > 0;
if (!hasMainChanges && !hasDetailChanges) {
toast('No changes detected');
return;
}
const finalPayload = {
TestMapID: formState.form.TestMapID,
...changedFields,
...(hasDetailChanges && {
details: {
created: tempMap.filter(r => !r.TestMapDetailID),
edited: updatedDetails,
deleted: deletedDetailIds
}
})
};
console.log(finalPayload);
const result = await formState.save(masterDetail.mode, finalPayload);
if (result.status === 'success') {
console.log('Test Map updated successfully');
toast('Test Map Updated!');
masterDetail.exitForm(true);
} else {
console.error('Failed to update test map:', result.message);
const errorMessages = result.messages ? Object.values(result.messages).join('\n') : 'Failed to update test map';
toast.error(errorMessages)
}
}
const primaryAction = $derived({
label: 'Save',
onClick: handleEdit,
disabled: helpers.hasErrors || formState.isSaving.current,
loading: formState.isSaving.current
});
const secondaryActions = [];
const mapFormFieldsTransformed = $derived.by(() => {
return formFields.map((group) => ({
...group,
rows: group.rows.map((row) => ({
...row,
columns: row.columns.map((col) => {
if (col.key === 'HostID') {
if (formState.form.HostType === 'HIS') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.HOSTAPP}`,
valueKey: 'HostAppID',
labelKey: 'HostAppName'
}
} else if (formState.form.HostType === 'SITE') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
valueKey: 'SiteID',
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`
};
} else if (formState.form.HostType === 'WST') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.WORKSTATION}`,
valueKey: 'WorkstationID',
labelKey: 'WorkstationName'
};
} else if (formState.form.HostType === 'INST') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.EQUIPMENT}`,
valueKey: 'EID',
labelKey: 'InstrumentName'
}
}
return { ...col, type: 'text', optionsEndpoint: undefined };
}
if (col.key === 'ClientID') {
if (formState.form.ClientType === 'HIS') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.HOSTAPP}`,
valueKey: 'HostAppID',
labelKey: 'HostAppName'
}
} else if (formState.form.ClientType === 'SITE') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
valueKey: 'SiteID',
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`
};
} else if (formState.form.ClientType === 'WST') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.WORKSTATION}`,
valueKey: 'WorkstationID',
labelKey: 'WorkstationName'
};
} else if (formState.form.ClientType === 'INST') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.EQUIPMENT}`,
valueKey: 'EID',
labelKey: 'InstrumentName'
}
}
return { ...col, type: 'text', optionsEndpoint: undefined };
}
return col;
})
}))
}));
});
const mapDetailFormFieldsTransformed = $derived.by(() => {
return testMapDetailFormFields.map((group) => ({
...group,
rows: group.rows.map((row) => ({
...row,
columns: row.columns.map((col) => {
if (col.key === 'HostTestCode') {
if (formState.form.HostType === 'SITE' && formState.form.HostID) {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.TEST}?SiteID=${formState.form.HostID}`,
valueKey: 'TestSiteCode',
labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`
};
}
return { ...col, type: 'text', optionsEndpoint: undefined };
}
if (col.key === 'ClientTestCode') {
if (formState.form.ClientType === 'SITE' && formState.form.ClientID) {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.TEST}?SiteID=${formState.form.ClientID}`,
valueKey: 'TestSiteCode',
labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`
};
}
return { ...col, type: 'text', optionsEndpoint: undefined };
}
return col
})
}))
}))
})
$effect(() => {
const mainForm = formState.form;
if (mainForm.details && Array.isArray(mainForm.details)) {
tempMap = mainForm.details.map((row, index) => ({
id: row.id ?? index + 1,
...row,
}));
}
})
$effect(() => {
const maxId = tempMap.reduce((max, row) => {
const rowId = typeof row.id === 'number' ? row.id : 0;
return rowId > max ? rowId : max;
}, 0);
if (maxId > idCounter) {
idCounter = maxId;
}
});
onMount(() => {
masterDetail.registerExtraState({
key: 'tempMap',
get: () => tempMap,
reset: () => { tempMap = []; idCounter = 0; editingId = null; },
});
});
$inspect(tempMap)
</script>
<FormPageContainer title="Edit Test Map" {primaryAction} {secondaryActions}>
<DictionaryFormRenderer
{formState}
formFields={mapFormFieldsTransformed}
mode="edit"
/>
<DictionaryFormRenderer
formState={testMapDetailFormState}
formFields={mapDetailFormFieldsTransformed}
mode="create"
/>
<div class="flex flex-col gap-4">
<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}
disabled={Object.values(testMapDetailFormState.errors).some(v => v !== null)}>
Insert
</Button>
{/if}
</div>
<div>
<Separator />
<Table.Root>
<Table.Header>
<Table.Row class="hover:bg-transparent">
<Table.Head>Host Test Code</Table.Head>
<Table.Head>Host Test Name</Table.Head>
<Table.Head>Client Test Code</Table.Head>
<Table.Head>Client Test Name</Table.Head>
<Table.Head>Container</Table.Head>
<Table.Head class="w-[80px]"></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if tempMap.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 tempMap as row (row.id)}
<Table.Row
class="cursor-pointer hover:bg-muted/50"
>
<Table.Cell>{row.HostTestCode}</Table.Cell>
<Table.Cell>{row.HostTestName}</Table.Cell>
<Table.Cell>{row.ClientTestCode}</Table.Cell>
<Table.Cell>{row.ClientTestName}</Table.Cell>
<Table.Cell>{row.ContainerLabel}</Table.Cell>
<Table.Cell>
<div class="flex gap-1">
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
<Button
size="icon"
variant="outline"
class="h-7 w-7 cursor-pointer"
onclick={() => handleEditDetail(row)}
>
<PencilIcon class="h-3.5 w-3.5" />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>
<p>Edit</p>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
<Button
size="icon"
variant="outline"
class="h-7 w-7 cursor-pointer"
onclick={() => {
selectedRow = row;
showDeleteConfirm = true;
}}
>
<Trash2Icon class="h-3.5 w-3.5" />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>
<p>Delete</p>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
</div>
</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body>
</Table.Root>
</div>
</div>
</FormPageContainer>
<ReusableAlertDialog
bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit}
/>
<ReusableAlertDialog
bind:open={showDeleteConfirm}
title="Delete Test Code?"
description="Are you sure you want to disable this test code ({selectedRow.HostTestCode})?"
confirmText="Delete"
onConfirm={() => {
handleRemoveDetail(selectedRow.id);
}}
/>