continue testmap & deleting testmap on test tab

This commit is contained in:
faiztyanirh 2026-04-09 16:39:10 +07:00
parent 801c0a54f1
commit 80d59f3120
14 changed files with 773 additions and 503 deletions

View File

@ -7,16 +7,10 @@
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 { 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";
@ -95,11 +89,6 @@
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]
);
@ -110,16 +99,6 @@
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 currentPayload = buildContactPayload({
mainForm: formState.form,
@ -279,7 +258,7 @@
function handleCancelEditDetail() {
resetContactDetailForm();
}
$inspect(deletedDetailIds)
function handleRemoveDetail(id) {
const row = tempDetailContact.find(r => r.id === id);
if (row?.ContactDetID) {

View File

@ -195,29 +195,6 @@ export const refTxtSchema = z
}
});
export const testMapSchema = z
.object({
HostID: z.string().optional(),
ClientID: z.string().optional()
})
.superRefine((data, ctx) => {
const hostID = data.HostID;
const clientID = data.ClientID;
if (hostID && clientID && hostID === clientID) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'ClientID must be different from HostID',
path: ['ClientID']
});
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'HostID must be different from ClientID',
path: ['HostID']
});
}
});
export const testInitialForm = {
TestSiteID: '',
SiteID: '',
@ -297,19 +274,6 @@ export const refTxtInitialForm = {
Notes: ''
};
export const testMapInitialForm = {
TestMapID: '',
HostType: '',
HostID: '',
HostTestCode: '',
HostTestName: '',
ClientType: '',
ClientID: '',
ClientTestCode: '',
ClientTestName: '',
ConDefID: ''
};
export const testDefaultErrors = {
TestSiteCode: 'Required',
TestSiteName: 'Required',
@ -340,11 +304,6 @@ export const refTxtDefaultErrors = {
AgeEnd: null
};
export const testMapDefaultErrors = {
HostID: null,
ClientID: null
};
export const testFormFields = [
{
title: 'Basic Information',
@ -947,101 +906,6 @@ export const refTxtFormFields = [
}
];
export const testMapFormFields = [
{
title: 'Host & Client Information',
rows: [
{
type: 'row',
columns: [
{
key: 'HostType',
label: 'Host Type',
required: false,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/entity_type`
},
{
key: 'ClientType',
label: 'Client Type',
required: false,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/entity_type`
}
]
},
{
type: 'row',
columns: [
{
key: 'HostID',
label: 'Host ID',
required: false,
type: 'text',
validateOn: ['input']
},
{
key: 'ClientID',
label: 'Client ID',
required: false,
type: 'text',
validateOn: ['input']
}
]
},
{
type: 'row',
columns: [
{
key: 'HostTestCode',
label: 'Host Test Code',
required: false,
type: 'text'
},
{
key: 'ClientTestCode',
label: 'Client Test Code',
required: false,
type: 'text'
}
]
},
{
type: 'row',
columns: [
{
key: 'HostTestName',
label: 'Host Test Name',
required: false,
type: 'text'
},
{
key: 'ClientTestName',
label: 'Client Test Name',
required: false,
type: 'text'
}
]
},
{
type: 'row',
columns: [
{
key: 'ConDefID',
label: 'Container Definition',
required: false,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.CONTAINER}`,
valueKey: 'ConDefID',
labelKey: (item) => `${item.ConCode} - ${item.ConName}`,
fullWidth: false
}
]
}
]
}
];
export function getTestFormActions(handlers) {
return [
{
@ -1058,7 +922,6 @@ export function buildTestPayload({
testType,
refNumData,
refTxtData,
mapData
}) {
let payload = {
...mainForm
@ -1085,19 +948,12 @@ export function buildTestPayload({
TestSiteID: Number(m.value)
}))
}
} else if (key === 'map' && mapData?.length > 0) {
payload.testmap = mapData;
} else if (key === 'cal') {
payload.testdefcal = {
...state.form,
FormulaInput: state.form?.FormulaInput?.map((m) => ({ TestSiteID: Number(m.testid) }))
};
}
// else if ((key === 'cal' || key === 'map') && state?.form) {
// payload[key] = {
// ...state.form
// };
// }
}
return cleanEmptyStrings(payload);

View File

@ -20,10 +20,6 @@
refTxtDefaultErrors,
refTxtInitialForm,
refTxtFormFields,
testMapSchema,
testMapInitialForm,
testMapDefaultErrors,
testMapFormFields,
testGroupSchema,
testGroupInitialForm,
testGroupDefaultErrors,
@ -33,7 +29,6 @@
import RefNum from './tabs/ref-num.svelte';
import RefTxt from './tabs/ref-txt.svelte';
import Calculation from './tabs/calculation.svelte';
import Map from './tabs/map.svelte';
import Group from './tabs/group.svelte';
import { API } from '$lib/config/api';
import { untrack } from 'svelte';
@ -42,10 +37,8 @@
let resetRefNum = $state();
let resetRefTxt = $state();
let resetMap = $state();
let refNumData = $state([]);
let refTxtData = $state([]);
let mapData = $state([]);
const { masterDetail, formFields, formActions, schema, initialForm } = props.context;
@ -75,13 +68,6 @@
defaultErrors: refTxtDefaultErrors
});
const mapFormState = useForm({
schema: testMapSchema,
initialForm: testMapInitialForm,
defaultErrors: testMapDefaultErrors,
modeOpt: 'cascade'
});
const activeFormStates = $derived.by(() => {
const testType = formState.form.TestType ?? '';
const refType = formState.form.RefType ?? '';
@ -99,18 +85,15 @@
case 'PARAM':
return {
...refState,
map: mapFormState
};
case 'CALC':
return {
cal: calFormState,
...refState,
map: mapFormState
};
case 'GROUP':
return {
group: groupFormState,
map: mapFormState
}
case 'TITLE':
default:
@ -137,7 +120,6 @@
async function handleSave() {
const mainForm = masterDetail.formState.form;
const testType = mainForm.TestType;
const cleanMapData = mapData.map(({ options, ...rest }) => rest);
const payload = buildTestPayload({
mainForm,
@ -145,7 +127,6 @@
testType: testType,
refNumData: refNumData,
refTxtData: refTxtData,
mapData: cleanMapData,
});
console.log(payload);
@ -172,11 +153,11 @@
switch (testType) {
case 'TEST':
case 'PARAM':
return ['definition', 'map', 'reference'];
return ['definition', 'reference'];
case 'CALC':
return ['definition', 'calculation', 'map', 'reference'];
return ['definition', 'calculation', 'reference'];
case 'GROUP':
return ['definition', 'group', 'map'];
return ['definition', 'group'];
default:
return ['definition'];
}
@ -258,81 +239,6 @@
}));
});
const testMapFormFieldsTransformed = $derived.by(() => {
return testMapFormFields.map((group) => ({
...group,
rows: group.rows.map((row) => ({
...row,
columns: row.columns.map((col) => {
if (col.key === 'HostID') {
if (mapFormState.form.HostType === 'SITE') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
valueKey: 'SiteID',
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`
};
}
return col;
}
if (col.key === 'ClientID') {
if (mapFormState.form.ClientType === 'SITE') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
valueKey: 'SiteID',
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`
};
}
return col;
}
if (col.key === 'HostTestCode' || col.key === 'HostTestName') {
if (mapFormState.form.HostType === 'SITE' && mapFormState.form.HostID) {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.TEST}?SiteID=${mapFormState.form.HostID}`,
valueKey: 'TestSiteID',
labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`
};
}
// kalau belum pilih HostID, kembalikan default (misal input biasa)
return {
...col,
type: 'text',
optionsEndpoint: undefined
};
}
if (col.key === 'ClientTestCode' || col.key === 'ClientTestName') {
if (mapFormState.form.ClientType === 'SITE' && mapFormState.form.ClientID) {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.TEST}?SiteID=${mapFormState.form.ClientID}`,
valueKey: 'TestSiteID',
labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`
};
}
// kalau belum pilih HostID, kembalikan default (misal input biasa)
return {
...col,
type: 'text',
optionsEndpoint: undefined
};
}
return col;
})
}))
}));
});
// $inspect(activeFormState.errors)
let activeTab = $state('definition');
$effect(() => {
@ -347,14 +253,6 @@
}
});
// $effect(() => {
// if (formState.form.Factor && !formState.form.Unit2) {
// formState.errors.Unit2 = 'Required';
// } else {
// formState.errors.Unit2 = null;
// }
// });
$effect(() => {
const allColumns = formFields.flatMap((section) =>
section.rows.flatMap((row) => row.columns ?? [])
@ -440,11 +338,6 @@
refTxtState.form.TxtRefType = value;
}
}
// $inspect({
// definition: formState.errors,
// active: Object.values(activeFormStates).map(fs => fs.errors)
// });
</script>
<FormPageContainer title="Create Test" {primaryAction} {secondaryActions} {actions}>
@ -462,9 +355,6 @@
{#if availableTabs.includes('reference')}
<Tabs.Trigger value="reference">Reference</Tabs.Trigger>
{/if}
{#if availableTabs.includes('map')}
<Tabs.Trigger value="map">Map</Tabs.Trigger>
{/if}
</Tabs.List>
<Tabs.Content value="definition">
<DictionaryFormRenderer
@ -485,9 +375,6 @@
<Tabs.Content value="group">
<Group {groupFormState} {testGroupFormFields} />
</Tabs.Content>
<Tabs.Content value="map">
<Map {mapFormState} testMapFormFields={testMapFormFieldsTransformed} bind:tempMap={mapData} bind:resetMap />
</Tabs.Content>
<Tabs.Content value="reference">
<div class="w-full h-full flex items-start">
{#if refComponent === 'numeric'}

View File

@ -20,10 +20,6 @@
refTxtDefaultErrors,
refTxtInitialForm,
refTxtFormFields,
testMapSchema,
testMapInitialForm,
testMapDefaultErrors,
testMapFormFields,
testGroupSchema,
testGroupInitialForm,
testGroupDefaultErrors,
@ -33,7 +29,6 @@
import RefNum from './tabs/ref-num.svelte';
import RefTxt from './tabs/ref-txt.svelte';
import Calculation from './tabs/calculation.svelte';
import Map from './tabs/map.svelte';
import Group from './tabs/group.svelte';
import { API } from "$lib/config/api";
import { untrack } from "svelte";
@ -43,10 +38,8 @@
let resetRefNum = $state();
let resetRefTxt = $state();
let resetMap = $state();
let refNumData = $state([]);
let refTxtData = $state([]);
let mapData = $state([]);
const { masterDetail, formFields, formActions, schema, initialForm } = props.context;
@ -76,13 +69,6 @@
defaultErrors: refTxtDefaultErrors
});
const mapFormState = useForm({
schema: testMapSchema,
initialForm: testMapInitialForm,
defaultErrors: testMapDefaultErrors,
modeOpt: 'cascade'
});
const activeFormStates = $derived.by(() => {
const testType = formState.form.TestType ?? '';
const refType = formState.form.RefType ?? '';
@ -100,13 +86,11 @@
case 'PARAM':
return {
...refState,
map: mapFormState
};
case 'CALC':
return {
cal: calFormState,
...refState,
map: mapFormState
};
case 'GROUP':
return {
@ -129,7 +113,6 @@
async function handleEdit() {
const mainForm = masterDetail.formState.form;
const testType = mainForm.TestType;
const cleanMapData = mapData.map(({ options, ...rest }) => rest);
const payload = buildTestPayload({
mainForm,
@ -137,7 +120,6 @@
testType: testType,
refNumData: refNumData,
refTxtData: refTxtData,
mapData: cleanMapData,
});
console.log(payload);
@ -162,9 +144,9 @@
switch (testType) {
case 'TEST':
case 'PARAM':
return ['definition', 'map', 'reference'];
return ['definition', 'reference'];
case 'CALC':
return ['definition', 'calculation', 'map', 'reference'];
return ['definition', 'calculation', 'reference'];
case 'GROUP':
return ['definition', 'group'];
default:
@ -248,80 +230,6 @@
}));
});
const testMapFormFieldsTransformed = $derived.by(() => {
return testMapFormFields.map((group) => ({
...group,
rows: group.rows.map((row) => ({
...row,
columns: row.columns.map((col) => {
if (col.key === 'HostID') {
if (mapFormState.form.HostType === 'SITE') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
valueKey: 'SiteID',
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`
};
}
return col;
}
if (col.key === 'ClientID') {
if (mapFormState.form.ClientType === 'SITE') {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.SITE}`,
valueKey: 'SiteID',
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`
};
}
return col;
}
if (col.key === 'HostTestCode' || col.key === 'HostTestName') {
if (mapFormState.form.HostType === 'SITE' && mapFormState.form.HostID) {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.TEST}?SiteID=${mapFormState.form.HostID}`,
valueKey: 'TestSiteID',
labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`
};
}
// kalau belum pilih HostID, kembalikan default (misal input biasa)
return {
...col,
type: 'text',
optionsEndpoint: undefined
};
}
if (col.key === 'ClientTestCode' || col.key === 'ClientTestName') {
if (mapFormState.form.ClientType === 'SITE' && mapFormState.form.ClientID) {
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.TEST}?SiteID=${mapFormState.form.ClientID}`,
valueKey: 'TestSiteID',
labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`
};
}
// kalau belum pilih HostID, kembalikan default (misal input biasa)
return {
...col,
type: 'text',
optionsEndpoint: undefined
};
}
return col;
})
}))
}));
});
let activeTab = $state('definition');
let groupInitialized = $state(false);
@ -343,7 +251,7 @@
AgeEnd: typeof row.AgeEnd === 'number' ? buildAgeText(daysToAge(row.AgeEnd)) : row.AgeEnd,
}));
}
// groupFormState.form.Members = [];
if (!groupInitialized && mainForm.testdefgrp?.members && Array.isArray(mainForm.testdefgrp.members)) {
groupFormState.form.Members = mainForm.testdefgrp.members.map((m, index) => ({
id: m.id ?? index + 1,
@ -351,12 +259,6 @@
}));
groupInitialized = true;
}
// if (mainForm.testmap && Array.isArray(mainForm.testmap)) {
// mapData = mainForm.testmap.map((row, index) => ({
// id: row.id ?? index + 1,
// ...row,
// }));
// }
})
$effect(() => {
@ -453,14 +355,6 @@
refTxtState.form.TxtRefType = value;
}
}
// $effect(() => {
// if (formState.form.Factor && !formState.form.Unit2) {
// formState.errors.Unit2 = 'Required';
// } else {
// formState.errors.Unit2 = null;
// }
// });
$effect(() => {
const refType = formState.form.RefType;
@ -488,9 +382,6 @@
{#if availableTabs.includes('reference')}
<Tabs.Trigger value="reference">Reference</Tabs.Trigger>
{/if}
{#if availableTabs.includes('map')}
<Tabs.Trigger value="map">Map</Tabs.Trigger>
{/if}
</Tabs.List>
<Tabs.Content value="definition">
<DictionaryFormRenderer
@ -511,9 +402,6 @@
<Tabs.Content value="group">
<Group {groupFormState} {testGroupFormFields} />
</Tabs.Content>
<Tabs.Content value="map">
<Map {mapFormState} testMapFormFields={testMapFormFieldsTransformed} bind:tempMap={mapData} bind:resetMap />
</Tabs.Content>
<Tabs.Content value="reference">
<div class="w-full h-full flex items-start">
{#if refComponent === 'numeric'}

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

@ -0,0 +1,185 @@
import { API } from '$lib/config/api';
import EraserIcon from '@lucide/svelte/icons/eraser';
import { z } from 'zod';
import { cleanEmptyStrings } from '$lib/utils/cleanEmptyStrings';
export const testMapSchema = z
.object({
HostID: z.string().optional(),
ClientID: z.string().optional()
})
// .superRefine((data, ctx) => {
// const hostID = data.HostID;
// const clientID = data.ClientID;
// if (hostID && clientID && hostID === clientID) {
// ctx.addIssue({
// code: z.ZodIssueCode.custom,
// message: 'ClientID must be different from HostID',
// path: ['ClientID']
// });
// ctx.addIssue({
// code: z.ZodIssueCode.custom,
// message: 'HostID must be different from ClientID',
// path: ['HostID']
// });
// }
// });
export const testMapDetailSchema = z.object({});
export const testMapInitialForm = {
TestMapID: '',
HostType: '',
HostID: '',
ClientType: '',
ClientID: '',
};
export const testMapDetailInitialForm = {
HostTestCode: '',
HostTestName: '',
ClientTestCode: '',
ClientTestName: '',
ConDefID: ''
}
export const testMapDefaultErrors = {
HostID: null,
ClientID: null
};
export const testMapDetailDefaultErrors = {};
export const testMapFormFields = [
{
rows: [
{
type: 'row',
columns: [
{
key: 'HostType',
label: 'Host Type',
required: false,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/entity_type`,
tabIndex: 1,
},
{
key: 'ClientType',
label: 'Client Type',
required: false,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/entity_type`,
tabIndex: 5,
}
]
},
{
type: 'row',
columns: [
{
key: 'HostID',
label: 'Host ID',
required: false,
type: 'text',
validateOn: ['input'],
tabIndex: 2,
},
{
key: 'ClientID',
label: 'Client ID',
required: false,
type: 'text',
validateOn: ['input'],
tabIndex: 6,
}
]
},
{
type: 'row',
columns: [
{
key: 'HostTestCode',
label: 'Host Test Code',
required: false,
type: 'text',
tabIndex: 3,
},
{
key: 'ClientTestCode',
label: 'Client Test Code',
required: false,
type: 'text',
tabIndex: 7,
}
]
},
{
type: 'row',
columns: [
{
key: 'HostTestName',
label: 'Host Test Name',
required: false,
type: 'text',
tabIndex: 4,
},
{
key: 'ClientTestName',
label: 'Client Test Name',
required: false,
type: 'text',
tabIndex: 8,
}
]
},
{
type: 'row',
columns: [
{
key: 'ConDefID',
label: 'Container Definition',
required: false,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.CONTAINER}`,
valueKey: 'ConDefID',
labelKey: (item) => `${item.ConCode} - ${item.ConName}`,
fullWidth: false,
tabIndex: 9,
}
]
}
]
}
];
export function getTestMapFormActions(handlers) {
return [
{
Icon: EraserIcon,
label: 'Clear Form',
onClick: handlers.clearForm
}
];
}
export function buildTestMapPayload({
mainForm,
tempMap,
}) {
const { HostTestCode, HostTestName, ClientTestCode, ClientTestName, ConDefID, ...rest } = mainForm;
let payload = {
...rest,
details: tempMap.map((item) => ({
HostTestCode: item.HostTestCode,
HostTestName: item.HostTestName,
ClientTestCode: item.ClientTestCode,
ClientTestName: item.ClientTestName,
ConDefID: item.ConDefID,
})),
};
return cleanEmptyStrings(payload);
}

View File

@ -26,24 +26,35 @@ export const testMapSchema = z
// }
// });
export const testMapDetailSchema = z.object({
ConDefID: z.string().trim().optional(),
});
export const testMapInitialForm = {
TestMapID: '',
HostType: '',
HostID: '',
HostTestCode: '',
HostTestName: '',
ClientType: '',
ClientID: '',
};
export const testMapDetailInitialForm = {
HostTestCode: '',
HostTestName: '',
ClientTestCode: '',
ClientTestName: '',
ConDefID: ''
};
}
export const testMapDefaultErrors = {
HostID: null,
ClientID: null
};
export const testMapDetailDefaultErrors = {
ConDefID: null,
};
export const testMapFormFields = [
{
rows: [
@ -89,6 +100,13 @@ export const testMapFormFields = [
}
]
},
]
}
];
export const testMapDetailFormFields = [
{
rows: [
{
type: 'row',
columns: [
@ -145,7 +163,7 @@ export const testMapFormFields = [
}
]
}
];
]
export function getTestMapFormActions(handlers) {
return [

View File

@ -4,6 +4,8 @@
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";
@ -11,23 +13,30 @@
import { Button } from "$lib/components/ui/button/index.js";
import { untrack } from "svelte";
import { API } from '$lib/config/api';
import { buildTestMapPayload } from "$lib/components/dictionary/testmap/config/testmap-form-config";
let props = $props();
const { masterDetail, formFields, formActions, schema } = props.context;
const { formState } = masterDetail;
let editingId = $state(null);
let idCounter = $state(0);
let tempMap = $state([]);
const { masterDetail, formFields, formActions, schema } = props.context;
const { formState } = masterDetail;
const testMapDetailFormState = useForm({
schema: testMapDetailSchema,
initialForm: testMapDetailInitialForm,
defaultErrors: testMapDetailDefaultErrors,
});
const helpers = useDictionaryForm(formState);
const handlers = {
clearForm: () => {
formState.reset();
resetTestMapDetailForm();
tempMap = [];
}
};
@ -37,7 +46,7 @@
function snapshotForm() {
return untrack(() => {
const f = formState.form;
const f = testMapDetailFormState.form;
return {
HostTestCode: f.HostTestCode ?? "",
HostTestName: f.HostTestName ?? "",
@ -48,24 +57,13 @@
});
}
function resetForm() {
formState.reset();
function resetTestMapDetailForm() {
testMapDetailFormState.reset();
editingId = null;
}
function resetTest() {
untrack(() => {
const f = formState.form;
f.HostTestCode = null;
f.HostTestName = null;
f.ClientTestCode = null;
f.ClientTestName = null;
f.ConDefID = null;
});
editingId = null;
}
function handleInsert() {
function handleInsertDetail() {
console.log('object');
const row = {
id: ++idCounter,
...snapshotForm()
@ -73,47 +71,38 @@
tempMap = [...tempMap, row];
resetTest();
resetTestMapDetailForm();
}
async function handleEdit(row) {
async function handleEditDetail(row) {
editingId = row.id;
untrack(() => {
const f = formState.form;
// f.HostType = row.HostType;
// f.HostID = row.HostID;
const f = testMapDetailFormState.form;
f.HostTestCode = row.HostTestCode;
f.HostTestName = row.HostTestName;
// f.ClientType = row.ClientType;
// f.ClientID = row.ClientID;
f.ClientTestCode = row.ClientTestCode;
f.ClientTestName = row.ClientTestName;
f.ConDefID = row.ConDefID;
// if (row.options) {
// for (const key in row.options) {
// masterDetail.formState.selectOption[key] = row.options[key];
// }
// }
});
}
function handleUpdate() {
function handleUpdateDetail() {
tempMap = tempMap.map((row) =>
row.id === editingId ? { id: row.id, ...snapshotForm() } : row
);
resetTest();
resetTestMapDetailForm();
}
function handleCancelEdit() {
resetTest();
function handleCancelEditDetail() {
resetTestMapDetailForm();
}
function handleRemove(id) {
function handleRemoveDetail(id) {
tempMap = tempMap.filter((row) => row.id !== id);
if (editingId === id) {
resetTest();
resetTestMapDetailForm();
}
}
@ -125,10 +114,10 @@
tempMap,
});
console.log(payload)
const result = await formState.save(masterDetail.mode, payload);
// const result = await formState.save(masterDetail.mode, payload);
toast('Test Map Created!');
masterDetail?.exitForm(true);
// toast('Test Map Created!');
// masterDetail?.exitForm(true);
}
const primaryAction = $derived({
@ -176,7 +165,15 @@
}
if (col.key === 'ClientID') {
if (formState.form.ClientType === 'SITE') {
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',
@ -184,55 +181,61 @@
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'
};
}
return { ...col, type: 'text', optionsEndpoint: undefined };
}
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: 'TestSiteID',
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: 'TestSiteID',
labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`
};
}
return {
...col,
type: 'text',
optionsEndpoint: undefined
};
}
return col;
})
}))
}));
});
// $effect(() => {
// if (formState.form.ClientTestCode) {
// formState.form.ClientTestName = 'nyaho';
// }
// })
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
})
}))
}))
})
</script>
<FormPageContainer title="Create Test Map" {primaryAction} {secondaryActions} {actions}>
@ -241,17 +244,22 @@
formFields={mapFormFieldsTransformed}
mode="create"
/>
<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={handleUpdate}>
<Button size="sm" class="cursor-pointer" onclick={handleUpdateDetail}>
Update
</Button>
<Button size="sm" variant="outline" class="cursor-pointer" onclick={handleCancelEdit}>
<Button size="sm" variant="outline" class="cursor-pointer" onclick={handleCancelEditDetail}>
Cancel
</Button>
{:else}
<Button size="sm" class="cursor-pointer" onclick={handleInsert}>
<Button size="sm" class="cursor-pointer" onclick={handleInsertDetail}>
Insert
</Button>
{/if}
@ -262,12 +270,8 @@
<Table.Root>
<Table.Header>
<Table.Row class="hover:bg-transparent">
<!-- <Table.Head>Host Type</Table.Head>
<Table.Head>Host ID</Table.Head> -->
<Table.Head>Host Test Code</Table.Head>
<Table.Head>Host Test Name</Table.Head>
<!-- <Table.Head>Client Type</Table.Head>
<Table.Head>Client ID</Table.Head> -->
<Table.Head>Client Test Code</Table.Head>
<Table.Head>Client Test Name</Table.Head>
<Table.Head>Container</Table.Head>
@ -297,7 +301,7 @@
size="icon"
variant="ghost"
class="h-7 w-7 cursor-pointer"
onclick={() => handleEdit(row)}
onclick={() => handleEditDetail(row)}
>
<PencilIcon class="h-3.5 w-3.5" />
</Button>
@ -305,7 +309,7 @@
size="icon"
variant="ghost"
class="h-7 w-7 cursor-pointer"
onclick={() => handleRemove(row.id)}
onclick={() => handleRemoveDetail(row.id)}
>
<Trash2Icon class="h-3.5 w-3.5" />
</Button>

View File

@ -1 +1,445 @@
ed
<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";
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([]);
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: 'Edit',
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'
};
}
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'
};
}
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;
}
});
</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}>
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.ConDefID}</Table.Cell>
<Table.Cell>
<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>
</div>
</FormPageContainer>
<ReusableAlertDialog
bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit}
/>

View File

@ -8,7 +8,7 @@
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
import MapIcon from "@lucide/svelte/icons/map";
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
@ -40,7 +40,7 @@
<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-start gap-4 tracking-widest font-semibold select-none h-full">
<ArrowLeftIcon />
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "BACK TO TEST MAP".split("") as c}
<span class="leading-none">{c}</span>

View File

@ -13,7 +13,7 @@
let testMap = $derived(masterDetail?.selectedItem?.data);
const handlers = {
editTest: () => masterDetail.enterEdit("data"),
editTestMap: () => masterDetail.enterEdit("data"),
};
const actions = viewActions(handlers);

View File

@ -3,6 +3,7 @@ import Settings2Icon from "@lucide/svelte/icons/settings-2";
import FlaskConicalIcon from "@lucide/svelte/icons/flask-conical";
import ActivityIcon from "@lucide/svelte/icons/activity";
import PencilIcon from "@lucide/svelte/icons/pencil";
import RefreshIcon from "@lucide/svelte/icons/refresh-cw";
import NotepadTextIcon from "@lucide/svelte/icons/notepad-text";
export const searchFields = [
@ -104,8 +105,13 @@ export const detailSections = [
}
]
export function patientActions(masterDetail, patientInitialForm) {
export function patientActions(masterDetail, handlers) {
return [
{
Icon: RefreshIcon,
label: 'Refresh Data',
onClick: handlers.refresh,
},
{
Icon: PlusIcon,
label: 'Add Patient',

View File

@ -7,13 +7,15 @@
import ReusableSearchParam from "$lib/components/reusable/reusable-search-param.svelte";
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
import ReusableDataTable from "$lib/components/reusable/reusable-data-table.svelte";
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
import MoveLeftIcon from "@lucide/svelte/icons/move-left";
let props = $props();
const search = useSearch(searchFields, searchParam);
const initialForm = props.masterDetail.formState.form;
const actions = patientActions(props.masterDetail, initialForm)
const handlers = {
refresh: () => alert('velom visa, savar!'),
};
const actions = patientActions(props.masterDetail, handlers)
actions.find(a => a.label === 'Search Parameters').popoverContent = searchParamSnippet;
let activeRowId = $state(null);
@ -36,7 +38,7 @@
<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-start gap-4 tracking-widest font-semibold select-none h-full">
<ArrowLeftIcon />
<MoveLeftIcon />
<div class="flex flex-col items-center justify-center flex-grow gap-4">
{#each "BACK TO PATIENT".split("") as c}
<span class="leading-none">{c}</span>

View File

@ -269,6 +269,7 @@
formState.form.ClientID = '';
formState.form.ClientTestCode = '';
formState.form.ClientTestName = '';
formState.selectOptions.ClientID = [];
formState.selectOptions.ClientTestCode = [];
formState.selectOptions.ClientTestName = [];
}