fix dynamic payload refnum & reftxt

This commit is contained in:
faiztyanirh 2026-03-07 16:01:15 +07:00
parent 104b6eff88
commit 3573b1b2dc
7 changed files with 871 additions and 866 deletions

View File

@ -73,6 +73,10 @@ export const testCalSchema = z
} }
}); });
export const testGroupSchema = z.object({
Member: z.string().optional()
});
export const refNumSchema = z export const refNumSchema = z
.object({ .object({
AgeStart: z.string().optional(), AgeStart: z.string().optional(),
@ -212,6 +216,12 @@ export const testCalInitialForm = {
FormulaCode: '' FormulaCode: ''
}; };
export const testGroupInitialForm = {
TestGrpID: '',
TestSiteID: '',
Member: '',
}
export const refNumInitialForm = { export const refNumInitialForm = {
RefNumID: '', RefNumID: '',
SiteID: '', SiteID: '',
@ -270,9 +280,13 @@ export const testDefaultErrors = {
export const testCalDefaultErrors = { export const testCalDefaultErrors = {
FormulaInput: 'Required', FormulaInput: 'Required',
FormulaCode: null FormulaCode: null,
}; };
export const testGroupDefaultErrors = {
Member: null,
}
export const refNumDefaultErrors = { export const refNumDefaultErrors = {
AgeStart: null, AgeStart: null,
AgeEnd: null, AgeEnd: null,
@ -597,6 +611,28 @@ export const testCalFormFields = [
} }
]; ];
export const testGroupFormFields = [
{
rows: [
{
type: 'row',
columns: [
{
key: 'Member',
label: 'Member Test',
required: false,
type: 'members',
optionsEndpoint: `${API.BASE_URL}${API.TEST}`,
valueKey: 'TestSiteCode',
labelKey: (item) => `${item.TestSiteCode} - ${item.TestSiteName}`,
validateOn: ['input']
}
]
},
]
}
];
export const refNumFormFields = [ export const refNumFormFields = [
{ {
rows: [ rows: [
@ -1013,21 +1049,25 @@ export function getTestFormActions(handlers) {
// return cleanEmptyStrings(payload); // return cleanEmptyStrings(payload);
// } // }
export function buildTestPayload({ mainForm, activeFormStates, testType }) { export function buildTestPayload({ mainForm, activeFormStates, testType, refNumData, refTxtData }) {
let payload = { let payload = {
...mainForm ...mainForm
}; };
for (const key in activeFormStates) { for (const key in activeFormStates) {
const state = activeFormStates[key]; const state = activeFormStates[key];
if (state?.form) { if (key === 'refNum' && refNumData?.length > 0) {
payload[key] = refNumData;
} else if (key === 'refTxt' && refTxtData?.length > 0) {
payload[key] = refTxtData;
} else if (state?.form) {
payload[key] = { payload[key] = {
...state.form ...state.form
}; };
} }
} }
return cleanEmptyStrings(payload);
return cleanEmptyStrings(payload);
} }

View File

@ -1,502 +1,524 @@
<script> <script>
import { useDictionaryForm } from "$lib/components/composable/use-dictionary-form.svelte"; import { useDictionaryForm } from '$lib/components/composable/use-dictionary-form.svelte';
import FormPageContainer from "$lib/components/reusable/form/form-page-container.svelte"; import FormPageContainer from '$lib/components/reusable/form/form-page-container.svelte';
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte"; import DictionaryFormRenderer from '$lib/components/reusable/form/dictionary-form-renderer.svelte';
import { toast } from "svelte-sonner"; import { toast } from 'svelte-sonner';
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte"; import ReusableAlertDialog from '$lib/components/reusable/reusable-alert-dialog.svelte';
import * as Tabs from "$lib/components/ui/tabs/index.js"; import * as Tabs from '$lib/components/ui/tabs/index.js';
import { useForm } from "$lib/components/composable/use-form.svelte"; import { useForm } from '$lib/components/composable/use-form.svelte';
import { buildTestPayload, import {
testCalSchema, testCalInitialForm, testCalDefaultErrors, testCalFormFields, buildTestPayload,
refNumSchema, refNumDefaultErrors, refNumInitialForm, refNumFormFields, testCalSchema,
refTxtSchema, refTxtDefaultErrors, refTxtInitialForm, refTxtFormFields, testCalInitialForm,
testMapSchema, testMapInitialForm, testMapDefaultErrors, testMapFormFields testCalDefaultErrors,
} from "$lib/components/dictionary/test/config/test-form-config"; testCalFormFields,
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte"; refNumSchema,
import RefNum from "./tabs/ref-num.svelte"; refNumDefaultErrors,
import RefTxt from "./tabs/ref-txt.svelte"; refNumInitialForm,
import Calculation from "./tabs/calculation.svelte"; refNumFormFields,
import Map from "./tabs/map.svelte"; refTxtSchema,
import { API } from "$lib/config/api"; refTxtDefaultErrors,
import { untrack } from "svelte"; refTxtInitialForm,
refTxtFormFields,
testMapSchema,
testMapInitialForm,
testMapDefaultErrors,
testMapFormFields,
testGroupSchema,
testGroupInitialForm,
testGroupDefaultErrors,
testGroupFormFields
} from '$lib/components/dictionary/test/config/test-form-config';
import ReusableEmpty from '$lib/components/reusable/reusable-empty.svelte';
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';
let props = $props(); let props = $props();
let resetRefNum = $state(); let resetRefNum = $state();
let resetRefTxt = $state(); let resetRefTxt = $state();
let resetMap = $state(); let resetMap = $state();
let refNumData = $state([]);
let refTxtData = $state([]);
const { masterDetail, formFields, formActions, schema, initialForm } = props.context; const { masterDetail, formFields, formActions, schema, initialForm } = props.context;
const { formState } = masterDetail; const { formState } = masterDetail;
const calFormState = useForm({ const calFormState = useForm({
schema: testCalSchema, schema: testCalSchema,
initialForm: testCalInitialForm, initialForm: testCalInitialForm,
defaultErrors: testCalDefaultErrors, defaultErrors: testCalDefaultErrors
});
const groupFormState = useForm({
schema: testGroupSchema,
initialForm: testGroupInitialForm,
defaultErrors: testGroupDefaultErrors,
}); });
const refNumState = useForm({ const refNumState = useForm({
schema: refNumSchema, schema: refNumSchema,
initialForm: refNumInitialForm, initialForm: refNumInitialForm,
defaultErrors: refNumDefaultErrors, defaultErrors: refNumDefaultErrors
}); });
const refTxtState = useForm({ const refTxtState = useForm({
schema: refTxtSchema, schema: refTxtSchema,
initialForm: refTxtInitialForm, initialForm: refTxtInitialForm,
defaultErrors: refTxtDefaultErrors, defaultErrors: refTxtDefaultErrors
}); });
const mapFormState = useForm({ const mapFormState = useForm({
schema: testMapSchema, schema: testMapSchema,
initialForm: testMapInitialForm, initialForm: testMapInitialForm,
defaultErrors: testMapDefaultErrors, defaultErrors: testMapDefaultErrors,
modeOpt: 'cascade' modeOpt: 'cascade'
}) });
// const activeFormStates = $derived.by(() => { // const activeFormStates = $derived.by(() => {
// switch (formState.form.TestType) { // switch (formState.form.TestType) {
// case "TEST": // case "TEST":
// case "PARAM": // case "PARAM":
// return [refNumState, refTxtState, mapFormState]; // return [refNumState, refTxtState, mapFormState];
// case "CALC": // case "CALC":
// return [calFormState, refNumState, refTxtState, mapFormState]; // return [calFormState, refNumState, refTxtState, mapFormState];
// case "GROUP": // case "GROUP":
// return []; // return [];
// case "TITLE": // case "TITLE":
// return []; // return [];
// default: // default:
// return []; // return [];
// } // }
// }); // });
const activeFormStates = $derived.by(() => { const activeFormStates = $derived.by(() => {
const testType = formState.form.TestType ?? ""; const testType = formState.form.TestType ?? '';
const refType = formState.form.RefType ?? ""; const refType = formState.form.RefType ?? '';
let refState = {}; let refState = {};
if (refType === "RANGE" || refType === "THOLD") { if (refType === 'RANGE' || refType === 'THOLD') {
refState = { refNum: refNumState }; refState = { refNum: refNumState };
} else if (refType === "TEXT" || refType === "VSET") { } else if (refType === 'TEXT' || refType === 'VSET') {
refState = { refTxt: refTxtState }; refState = { refTxt: refTxtState };
} }
switch (testType) { switch (testType) {
case "TEST": case 'TEST':
case "PARAM": case 'PARAM':
return { return {
...refState, ...refState,
map: mapFormState map: mapFormState
}; };
case "CALC": case 'CALC':
return { return {
cal: calFormState, cal: calFormState,
...refState, ...refState,
map: mapFormState map: mapFormState
}; };
case "GROUP": case 'GROUP':
case "TITLE": case 'TITLE':
default: default:
return {}; return {};
} }
}); });
// const helpers = useDictionaryForm(formState); // const helpers = useDictionaryForm(formState);
const helpers = useDictionaryForm(formState, () => activeFormStates); const helpers = useDictionaryForm(formState, () => activeFormStates);
const handlers = { const handlers = {
clearForm: () => { clearForm: () => {
formState.reset(); formState.reset();
} }
}; };
const actions = formActions(handlers); const actions = formActions(handlers);
const allColumns = formFields.flatMap( const allColumns = formFields.flatMap((section) =>
(section) => section.rows.flatMap( section.rows.flatMap((row) => row.columns ?? [])
(row) => row.columns ?? [] );
)
);
let showConfirm = $state(false); let showConfirm = $state(false);
async function handleSave() { async function handleSave() {
const mainForm = masterDetail.formState.form; const mainForm = masterDetail.formState.form;
const testType = mainForm.TestType; const testType = mainForm.TestType;
const payload = buildTestPayload({ const payload = buildTestPayload({
mainForm, mainForm,
activeFormStates, activeFormStates,
testType: testType testType: testType,
}); refNumData: refNumData,
console.log(payload); refTxtData: refTxtData
// const result = await formState.save(masterDetail.mode); });
console.log(payload);
// const result = await formState.save(masterDetail.mode);
// toast('Test Created!'); // toast('Test Created!');
// masterDetail?.exitForm(true); // masterDetail?.exitForm(true);
} }
const primaryAction = $derived({ const primaryAction = $derived({
label: 'Save', label: 'Save',
onClick: handleSave, onClick: handleSave,
disabled: helpers.hasErrors || formState.isSaving.current, disabled: helpers.hasErrors || formState.isSaving.current,
loading: formState.isSaving.current loading: formState.isSaving.current
}); });
const secondaryActions = []; const secondaryActions = [];
// ================================================================== // ==================================================================
let availableTabs = $derived.by(() => {
const testType = formState?.form?.TestType;
switch(testType) {
case 'TEST':
case 'PARAM':
return ['definition', 'map', 'reference'];
case 'CALC':
return ['definition', 'calculation', 'map', 'reference'];
case 'GROUP':
return ['definition', 'group'];
default:
return ['definition'];
}
});
let disabledResultTypes = $derived.by(() => { let availableTabs = $derived.by(() => {
const testType = formState?.form?.TestType; const testType = formState?.form?.TestType;
switch(testType) {
case 'TEST':
case 'PARAM':
return [];
case 'CALC':
return ['RANGE', 'TEXT', 'VSET', 'NORES'];
case 'GROUP':
return ['NMRIC', 'RANGE', 'TEXT', 'VSET', 'NORES'];
case 'TITLE':
return ['NMRIC', 'RANGE', 'TEXT', 'VSET', 'NORES'];
default:
return [];
}
});
let disabledReferenceTypes = $derived.by(() => { switch (testType) {
const resultType = formState?.form?.ResultType; case 'TEST':
case 'PARAM':
switch(resultType) { return ['definition', 'map', 'reference'];
case 'NMRIC': case 'CALC':
return ['TEXT', 'VSET', 'NOREF']; return ['definition', 'calculation', 'map', 'reference'];
case 'RANGE': case 'GROUP':
return ['TEXT', 'VSET', 'NOREF']; return ['definition', 'group'];
case 'TEXT': default:
return ['RANGE', 'THOLD', 'VSET', 'NOREF']; return ['definition'];
case 'VSET': }
return ['RANGE', 'THOLD', 'TEXT', 'NOREF']; });
case 'NORES':
return ['RANGE', 'THOLD', 'TEXT', 'VSET'];
default:
return ['RANGE', 'THOLD', 'TEXT', 'VSET', 'NOREF'];
}
});
let hiddenFields = $derived.by(() => { let disabledResultTypes = $derived.by(() => {
const resultType = formState?.form?.ResultType; const testType = formState?.form?.TestType;
return resultType !== "VSET" ? ["VSet"] : [];
});
let refComponent = $derived.by(() => { switch (testType) {
const refType = formState.form.RefType; case 'TEST':
if (refType === 'RANGE' || refType === 'THOLD') return 'numeric'; case 'PARAM':
if (refType === 'TEXT' || refType === 'VSET') return 'text'; return [];
return null; case 'CALC':
}); return ['RANGE', 'TEXT', 'VSET', 'NORES'];
case 'GROUP':
return ['NMRIC', 'RANGE', 'TEXT', 'VSET', 'NORES'];
case 'TITLE':
return ['NMRIC', 'RANGE', 'TEXT', 'VSET', 'NORES'];
default:
return [];
}
});
const refTxtFormFieldsTransformed = $derived.by(() => { let disabledReferenceTypes = $derived.by(() => {
return refTxtFormFields.map(group => ({ const resultType = formState?.form?.ResultType;
...group,
rows: group.rows.map(row => ({
...row,
columns: row.columns.map(col => {
if (col.key !== "RefTxt") return col;
if (formState.form.ResultType !== "VSET" || !formState.form.VSet) { switch (resultType) {
return { case 'NMRIC':
...col, return ['TEXT', 'VSET', 'NOREF'];
type: "textarea", case 'RANGE':
optionsEndpoint: undefined return ['TEXT', 'VSET', 'NOREF'];
}; case 'TEXT':
} return ['RANGE', 'THOLD', 'VSET', 'NOREF'];
case 'VSET':
return ['RANGE', 'THOLD', 'TEXT', 'NOREF'];
case 'NORES':
return ['RANGE', 'THOLD', 'TEXT', 'VSET'];
default:
return ['RANGE', 'THOLD', 'TEXT', 'VSET', 'NOREF'];
}
});
return { let hiddenFields = $derived.by(() => {
...col, const resultType = formState?.form?.ResultType;
type: "select", return resultType !== 'VSET' ? ['VSet'] : [];
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/${formState.form.VSet}`, });
fullWidth: false
};
})
}))
}));
});
const testMapFormFieldsTransformed = $derived.by(() => { let refComponent = $derived.by(() => {
return testMapFormFields.map(group => ({ const refType = formState.form.RefType;
...group, if (refType === 'RANGE' || refType === 'THOLD') return 'numeric';
rows: group.rows.map(row => ({ if (refType === 'TEXT' || refType === 'VSET') return 'text';
...row, return null;
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") { const refTxtFormFieldsTransformed = $derived.by(() => {
if (mapFormState.form.ClientType === "SITE") { return refTxtFormFields.map((group) => ({
return { ...group,
...col, rows: group.rows.map((row) => ({
type: "select", ...row,
optionsEndpoint: `${API.BASE_URL}${API.SITE}`, columns: row.columns.map((col) => {
valueKey: "SiteID", if (col.key !== 'RefTxt') return col;
labelKey: (item) => `${item.SiteCode} - ${item.SiteName}`,
};
}
return col;
}
if (col.key === "HostTestCode" || col.key === "HostTestName") { if (formState.form.ResultType !== 'VSET' || !formState.form.VSet) {
if (mapFormState.form.HostType === "SITE" && mapFormState.form.HostID) { return {
return { ...col,
...col, type: 'textarea',
type: "select", optionsEndpoint: undefined
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") { return {
if (mapFormState.form.ClientType === "SITE" && mapFormState.form.ClientID) { ...col,
return { type: 'select',
...col, optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/${formState.form.VSet}`,
type: "select", fullWidth: false
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; 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;
}
// $inspect(activeFormState.errors) if (col.key === 'ClientID') {
let activeTab = $state('definition'); 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;
}
$effect(() => { if (col.key === 'HostTestCode' || col.key === 'HostTestName') {
if (!availableTabs.includes(activeTab)) { if (mapFormState.form.HostType === 'SITE' && mapFormState.form.HostID) {
activeTab = availableTabs[0]; 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
};
}
$effect(() => { if (col.key === 'ClientTestCode' || col.key === 'ClientTestName') {
for (const key of hiddenFields) { if (mapFormState.form.ClientType === 'SITE' && mapFormState.form.ClientID) {
formState.form[key] = ""; 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
};
}
$effect(() => { return col;
if (formState.form.Factor && !formState.form.Unit2) { })
formState.errors.Unit2 = "Required"; }))
} else { }));
formState.errors.Unit2 = null; });
}
});
$effect(() => { // $inspect(activeFormState.errors)
const allColumns = formFields.flatMap( let activeTab = $state('definition');
(section) => section.rows.flatMap(
(row) => row.columns ?? []
)
);
untrack(() => {
for (const col of allColumns) {
if (!col.optionsEndpoint) continue;
if (!col.optionsEndpoint || col.autoFetch === false) continue;
formState.fetchOptions?.(
{
key: col.key,
optionsEndpoint: col.optionsEndpoint,
valueKey: col.valueKey,
labelKey: col.labelKey,
},
formState.form
);
}
})
});
function handleTestTypeChange(value) { $effect(() => {
formState.form.TestType = value; if (!availableTabs.includes(activeTab)) {
activeTab = availableTabs[0];
}
});
formState.form.ResultType = ""; $effect(() => {
formState.errors.ResultType = null; for (const key of hiddenFields) {
formState.form.RefType = ""; formState.form[key] = '';
formState.errors.RefType = null; }
});
calFormState.reset(); $effect(() => {
refNumState.reset(); if (formState.form.Factor && !formState.form.Unit2) {
refTxtState.reset(); formState.errors.Unit2 = 'Required';
} else {
formState.errors.Unit2 = null;
}
});
resetRefNum?.(); $effect(() => {
resetRefTxt?.(); const allColumns = formFields.flatMap((section) =>
} section.rows.flatMap((row) => row.columns ?? [])
);
function handleResultTypeChange(value) { untrack(() => {
formState.form.ResultType = value; for (const col of allColumns) {
if (!col.optionsEndpoint) continue;
if (!col.optionsEndpoint || col.autoFetch === false) continue;
formState.form.RefType = ""; formState.fetchOptions?.(
formState.errors.RefType = null; {
key: col.key,
optionsEndpoint: col.optionsEndpoint,
valueKey: col.valueKey,
labelKey: col.labelKey
},
formState.form
);
}
});
});
calFormState.reset(); function handleTestTypeChange(value) {
refNumState.reset(); formState.form.TestType = value;
refTxtState.reset();
resetRefNum?.(); formState.form.ResultType = '';
resetRefTxt?.(); formState.errors.ResultType = null;
formState.form.RefType = '';
formState.errors.RefType = null;
let newRefType = ""; calFormState.reset();
if (value === 'TEXT') { refNumState.reset();
newRefType = 'TEXT'; refTxtState.reset();
}
if (value === 'VSET') {
newRefType = 'VSET';
}
if (value === 'NORES') {
newRefType = 'NOREF';
}
if (newRefType) {
formState.form.RefType = newRefType;
handleRefTypeChange(newRefType);
}
}
function handleRefTypeChange(value) { resetRefNum?.();
formState.form.RefType = value; resetRefTxt?.();
}
refNumState.reset(); function handleResultTypeChange(value) {
refTxtState.reset(); formState.form.ResultType = value;
resetRefNum?.(); formState.form.RefType = '';
resetRefTxt?.(); formState.errors.RefType = null;
if (value === 'RANGE' || value === 'THOLD') { calFormState.reset();
refNumState.form.NumRefType = value; refNumState.reset();
} refTxtState.reset();
if (value === 'TEXT' || value === 'VSET') {
refTxtState.form.TxtRefType = value;
}
}
// $inspect({ resetRefNum?.();
// definition: formState.errors, resetRefTxt?.();
// active: activeFormStates.map(fs => fs.errors)
// });
// $inspect({ let newRefType = '';
// definition: formState.errors, if (value === 'TEXT') {
// active: Object.values(activeFormStates).map(fs => fs.errors) newRefType = 'TEXT';
// }); }
if (value === 'VSET') {
newRefType = 'VSET';
}
if (value === 'NORES') {
newRefType = 'NOREF';
}
if (newRefType) {
formState.form.RefType = newRefType;
handleRefTypeChange(newRefType);
}
}
function handleRefTypeChange(value) {
formState.form.RefType = value;
refNumState.reset();
refTxtState.reset();
resetRefNum?.();
resetRefTxt?.();
if (value === 'RANGE' || value === 'THOLD') {
refNumState.form.NumRefType = value;
}
if (value === 'TEXT' || value === 'VSET') {
refTxtState.form.TxtRefType = value;
}
}
// $inspect({
// definition: formState.errors,
// active: activeFormStates.map(fs => fs.errors)
// });
// $inspect({
// definition: formState.errors,
// active: Object.values(activeFormStates).map(fs => fs.errors)
// });
</script> </script>
<FormPageContainer title="Create Test" {primaryAction} {secondaryActions} {actions}> <FormPageContainer title="Create Test" {primaryAction} {secondaryActions} {actions}>
<Tabs.Root bind:value={activeTab} class="w-full h-full"> <Tabs.Root bind:value={activeTab} class="w-full h-full">
<Tabs.List> <Tabs.List>
{#if availableTabs.includes('definition')} {#if availableTabs.includes('definition')}
<Tabs.Trigger value="definition">Definition</Tabs.Trigger> <Tabs.Trigger value="definition">Definition</Tabs.Trigger>
{/if} {/if}
{#if availableTabs.includes('calculation')} {#if availableTabs.includes('calculation')}
<Tabs.Trigger value="calculation">Calculation</Tabs.Trigger> <Tabs.Trigger value="calculation">Calculation</Tabs.Trigger>
{/if} {/if}
{#if availableTabs.includes('group')} {#if availableTabs.includes('group')}
<Tabs.Trigger value="group">Group</Tabs.Trigger> <Tabs.Trigger value="group">Group</Tabs.Trigger>
{/if} {/if}
{#if availableTabs.includes('reference')} {#if availableTabs.includes('reference')}
<Tabs.Trigger value="reference">Reference</Tabs.Trigger> <Tabs.Trigger value="reference">Reference</Tabs.Trigger>
{/if} {/if}
{#if availableTabs.includes('map')} {#if availableTabs.includes('map')}
<Tabs.Trigger value="map">Map</Tabs.Trigger> <Tabs.Trigger value="map">Map</Tabs.Trigger>
{/if} {/if}
</Tabs.List> </Tabs.List>
<Tabs.Content value="definition"> <Tabs.Content value="definition">
<DictionaryFormRenderer <DictionaryFormRenderer
{formState} {formState}
formFields={formFields} {formFields}
mode="create" mode="create"
{disabledResultTypes} {disabledResultTypes}
{disabledReferenceTypes} {disabledReferenceTypes}
{hiddenFields} {hiddenFields}
{handleTestTypeChange} {handleTestTypeChange}
{handleResultTypeChange} {handleResultTypeChange}
{handleRefTypeChange} {handleRefTypeChange}
/> />
</Tabs.Content>
<Tabs.Content value="calculation">
<Calculation {calFormState} {testCalFormFields} />
</Tabs.Content>
<Tabs.Content value="group">
<Group {groupFormState} {testGroupFormFields} />
</Tabs.Content> </Tabs.Content>
<Tabs.Content value="calculation"> <Tabs.Content value="map">
<Calculation {calFormState} {testCalFormFields} /> <Map {mapFormState} testMapFormFields={testMapFormFieldsTransformed} bind:resetMap />
</Tabs.Content> </Tabs.Content>
<Tabs.Content value="group"> <Tabs.Content value="reference">
group <div class="w-full h-full flex items-start">
</Tabs.Content> {#if refComponent === 'numeric'}
<Tabs.Content value="map"> <RefNum {refNumState} {refNumFormFields} bind:tempNumeric={refNumData} bind:resetRefNum />
<Map {mapFormState} testMapFormFields={testMapFormFieldsTransformed} bind:resetMap={resetMap}/> {:else if refComponent === 'text'}
</Tabs.Content> <RefTxt {refTxtState} refTxtFormFields={refTxtFormFieldsTransformed} bind:tempTxt={refTxtData} bind:resetRefTxt />
<Tabs.Content value="reference"> {:else}
<div class="w-full h-full flex items-start"> <div class="h-full w-full flex items-center">
{#if refComponent === 'numeric'} <ReusableEmpty desc="Select a Reference Type" />
<RefNum {refNumState} {refNumFormFields} bind:resetRefNum={resetRefNum}/> </div>
{:else if refComponent === 'text'} {/if}
<RefTxt {refTxtState} refTxtFormFields={refTxtFormFieldsTransformed} bind:resetRefTxt={resetRefTxt} /> </div>
{:else} </Tabs.Content>
<div class="h-full w-full flex items-center"> </Tabs.Root>
<ReusableEmpty desc="Select a Reference Type" />
</div>
{/if}
</div>
</Tabs.Content>
</Tabs.Root>
</FormPageContainer> </FormPageContainer>
<ReusableAlertDialog <ReusableAlertDialog
bind:open={masterDetail.showExitConfirm} bind:open={masterDetail.showExitConfirm}
onConfirm={masterDetail.confirmExit} onConfirm={masterDetail.confirmExit}
/> />

View File

@ -9,154 +9,11 @@
import DictionaryFormRenderer from '$lib/components/reusable/form/dictionary-form-renderer.svelte'; import DictionaryFormRenderer from '$lib/components/reusable/form/dictionary-form-renderer.svelte';
let props = $props(); let props = $props();
// const formState = props.calFormState;
// let options = $state([]);
// let isLoading = $state(false);
// let errors = $state({});
// function hasExactKeyword(input, keyword) {
// const regex = new RegExp(`\\b${keyword}\\b`, 'i');
// return regex.test(input);
// }
// // 🔹 FETCH OPTIONS
// async function fetchTests() {
// isLoading = true;
// try {
// const res = await fetch(`${API.BASE_URL}${API.TEST}`);
// const data = await res.json();
// console.log(data);
// options = data.data.map((item) => ({
// value: item.TestSiteCode,
// label: `${item.TestSiteCode} - ${item.TestSiteName}`
// }));
// } catch (err) {
// console.error('Failed to fetch tests', err);
// } finally {
// isLoading = false;
// }
// }
// $effect(() => {
// fetchTests();
// });
// // 🔹 VALIDATION
// $effect(() => {
// const result = testCalSchema.safeParse(formState.form);
// if (!result.success) {
// const fieldErrors = {};
// for (const issue of result.error.issues) {
// fieldErrors[issue.path[0]] = issue.message;
// }
// errors = fieldErrors;
// } else {
// errors = {};
// }
// });
// // 🔹 Badge status
// const inputStatus = $derived.by(() => {
// const inputs = formState.form.FormulaInput || [];
// const code = formState.form.FormulaCode || '';
// return inputs.map((v) => ({
// value: v,
// done: hasExactKeyword(code, v)
// }));
// });
// function unselectAll() {
// formState.form.FormulaInput = [];
// errors = {};
// }
</script> </script>
<div class="flex flex-col gap-4 w-full"> <div class="flex flex-col gap-4 w-full">
<DictionaryFormRenderer formState={props.calFormState} formFields={props.testCalFormFields}/> <DictionaryFormRenderer formState={props.calFormState} formFields={props.testCalFormFields}/>
</div> </div>
<!-- <div class="p-2 space-y-6">
<div class="grid grid-cols-1 space-y-2 gap-6 md:gap-4">
<div class="flex w-full flex-col gap-1.5">
<div class="flex justify-between items-center w-full">
<Label>Formula Input</Label>
</div>
<div class="relative flex flex-col items-center w-full">
<Select.Root
type="multiple"
bind:value={formState.form.FormulaInput}
>
<Select.Trigger class="w-full">
{formState.form.FormulaInput?.length
? options
.filter(o => formState.form.FormulaInput.includes(o.value))
.map(o => o.label)
.join(', ')
: 'Select parameters'}
</Select.Trigger>
<Select.Content>
<Select.Group>
{#if isLoading}
<div class="p-2 text-sm text-muted-foreground">
Loading...
</div>
{:else}
{#if formState.form.FormulaInput.length > 0}
<Select.Separator />
<button
class="w-full px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground"
onclick={unselectAll}
>
Unselect All
</button>
{/if}
{#each options as opt (opt.value)}
<Select.Item value={opt.value} label={opt.label}>
{opt.label}
</Select.Item>
{/each}
{/if}
</Select.Group>
</Select.Content>
</Select.Root>
</div>
</div>
<div class="flex w-full flex-col gap-1.5">
<div class="flex justify-between items-center w-full">
<Label>Formula Code</Label>
</div>
<div class="relative flex flex-col items-center w-full">
<Textarea
class="border rounded p-2"
bind:value={formState.form.FormulaCode}
/>
</div>
</div>
</div>
{#if errors.FormulaCode}
<div class="flex flex-col gap-2 text-sm text-destructive">
<span>{errors.FormulaCode}</span>
<div class="flex gap-1">
{#each inputStatus as item (item.value)}
<Badge variant={item.done ? 'default' : 'destructive'}>
{item.value}
</Badge>
{/each}
</div>
</div>
{/if}
</div> -->

View File

@ -0,0 +1,25 @@
<script>
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte";
let props = $props();
let members = $state([{ id: 1, value: "" }]);
function addMember() {
members = [...members, { id: Date.now(), value: "" }];
}
function removeMember(id) {
members = members.filter(m => m.id !== id);
}
</script>
<div class="flex flex-col gap-4 w-full">
<DictionaryFormRenderer
formState={props.groupFormState}
formFields={props.testGroupFormFields}
{members}
onAddMember={addMember}
onRemoveMember={removeMember}
/>
</div>

View File

@ -1,307 +1,301 @@
<script> <script>
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte"; import DictionaryFormRenderer from '$lib/components/reusable/form/dictionary-form-renderer.svelte';
import { Separator } from "$lib/components/ui/separator/index.js"; import { Separator } from '$lib/components/ui/separator/index.js';
import * as Table from "$lib/components/ui/table/index.js"; import * as Table from '$lib/components/ui/table/index.js';
import { Button } from "$lib/components/ui/button/index.js"; import { Button } from '$lib/components/ui/button/index.js';
import { Badge } from "$lib/components/ui/badge/index.js"; import { Badge } from '$lib/components/ui/badge/index.js';
import { buildAgeText } from "$lib/utils/ageUtils"; import { buildAgeText } from '$lib/utils/ageUtils';
import PencilIcon from "@lucide/svelte/icons/pencil"; import PencilIcon from '@lucide/svelte/icons/pencil';
import Trash2Icon from "@lucide/svelte/icons/trash-2"; import Trash2Icon from '@lucide/svelte/icons/trash-2';
import { untrack } from "svelte"; import { untrack } from 'svelte';
import { toDays } from "$lib/utils/ageUtils"; import { toDays } from '$lib/utils/ageUtils';
let { resetRefNum = $bindable(), ...props } = $props() let { tempNumeric = $bindable([]), resetRefNum = $bindable(), ...props } = $props();
let tempNumeric = $state([]); let editingId = $state(null);
let editingId = $state(null); let idCounter = $state(0);
let idCounter = $state(0);
resetRefNum = () => { resetRefNum = () => {
tempNumeric = []; tempNumeric = [];
}; };
let joinFields = $state({ let joinFields = $state({
AgeStart: { DD: "", MM: "", YY: "" }, AgeStart: { DD: '', MM: '', YY: '' },
AgeEnd: { DD: "", MM: "", YY: "" }, AgeEnd: { DD: '', MM: '', YY: '' }
}); });
let disabledSign = $derived.by(() => { let disabledSign = $derived.by(() => {
const refType = props.refNumState.form.NumRefType; const refType = props.refNumState.form.NumRefType;
if (refType === "RANGE") return true;
if (refType === "THOLD") return false;
return false;
});
function snapshotForm() {
const f = props.refNumState.form;
return {
SpcType: f.SpcType ?? "",
Sex: f.Sex ?? "",
AgeStart: f.AgeStart ?? "",
AgeEnd: f.AgeEnd ?? "",
NumRefType: f.NumRefType ?? "",
RangeType: f.RangeType ?? "",
LowSign: f.LowSign ?? "",
Low: f.Low ?? "",
HighSign: f.HighSign ?? "",
High: f.High ?? "",
Display: f.Display ?? "",
Flag: f.Flag ?? "",
Interpretation: f.Interpretation ?? "",
Notes: f.Notes ?? "",
};
}
function resetForm() { if (refType === 'RANGE') return true;
props.refNumState.reset?.(); if (refType === 'THOLD') return false;
joinFields = {
AgeStart: { DD: "", MM: "", YY: "" },
AgeEnd: { DD: "", MM: "", YY: "" },
};
editingId = null;
}
function handleInsert() { return false;
// console.log(props.refNumState.form); });
// const low = Number(props.refNumState.form.Low);
// const high = Number(props.refNumState.form.High);
const newStart = toDays(props.refNumState.form.AgeStart);
const newEnd = toDays(props.refNumState.form.AgeEnd);
// const row = { id: ++idCounter, ...snapshotForm() };
// tempNumeric = [...tempNumeric, row];
// resetForm();
const isOverlap = tempNumeric.some(row => { function snapshotForm() {
const existingStart = toDays(row.AgeStart); const f = props.refNumState.form;
const existingEnd = toDays(row.AgeEnd); return {
SpcType: f.SpcType ?? '',
Sex: f.Sex ?? '',
AgeStart: f.AgeStart ?? '',
AgeEnd: f.AgeEnd ?? '',
NumRefType: f.NumRefType ?? '',
RangeType: f.RangeType ?? '',
LowSign: f.LowSign ?? '',
Low: f.Low ?? '',
HighSign: f.HighSign ?? '',
High: f.High ?? '',
Display: f.Display ?? '',
Flag: f.Flag ?? '',
Interpretation: f.Interpretation ?? '',
Notes: f.Notes ?? ''
};
}
if (existingStart == null || existingEnd == null) return false; function resetForm() {
props.refNumState.reset?.();
joinFields = {
AgeStart: { DD: '', MM: '', YY: '' },
AgeEnd: { DD: '', MM: '', YY: '' }
};
editingId = null;
}
return !(newEnd < existingStart || newStart > existingEnd); function handleInsert() {
}); // console.log(props.refNumState.form);
// const low = Number(props.refNumState.form.Low);
// const high = Number(props.refNumState.form.High);
const newStart = toDays(props.refNumState.form.AgeStart);
const newEnd = toDays(props.refNumState.form.AgeEnd);
// const row = { id: ++idCounter, ...snapshotForm() };
// tempNumeric = [...tempNumeric, row];
// resetForm();
if (isOverlap) { const isOverlap = tempNumeric.some((row) => {
props.refNumState.errors.AgeEnd = const existingStart = toDays(row.AgeStart);
"Age range overlaps with existing data"; const existingEnd = toDays(row.AgeEnd);
return;
}
const row = { if (existingStart == null || existingEnd == null) return false;
id: ++idCounter,
...snapshotForm()
};
tempNumeric = [...tempNumeric, row]; return !(newEnd < existingStart || newStart > existingEnd);
});
resetForm(); if (isOverlap) {
} props.refNumState.errors.AgeEnd = 'Age range overlaps with existing data';
return;
}
function handleEdit(row) { const row = {
editingId = row.id; id: ++idCounter,
...snapshotForm()
};
const f = props.refNumState.form; tempNumeric = [...tempNumeric, row];
f.SpcType = row.SpcType;
f.Sex = row.Sex;
f.AgeStart = row.AgeStart;
f.AgeEnd = row.AgeEnd;
f.NumRefType = row.NumRefType;
f.RangeType = row.RangeType;
f.LowSign = row.LowSign;
f.Low = row.Low;
f.HighSign = row.HighSign;
f.High = row.High;
f.Display = row.Display;
f.Flag = row.Flag;
f.Interpretation = row.Interpretation;
f.Notes = row.Notes;
for (const key of ["AgeStart", "AgeEnd"]) { resetForm();
const val = row[key] ?? ""; }
const match = val.match(/(\d+)Y\s*(\d+)M\s*(\d+)D/);
joinFields[key] = match
? { YY: match[1], MM: match[2], DD: match[3] }
: { DD: "", MM: "", YY: "" };
}
}
function handleUpdate() { function handleEdit(row) {
tempNumeric = tempNumeric.map((row) => editingId = row.id;
row.id === editingId ? { id: row.id, ...snapshotForm() } : row
);
resetForm();
}
function handleCancelEdit() { const f = props.refNumState.form;
resetForm(); f.SpcType = row.SpcType;
} f.Sex = row.Sex;
f.AgeStart = row.AgeStart;
f.AgeEnd = row.AgeEnd;
f.NumRefType = row.NumRefType;
f.RangeType = row.RangeType;
f.LowSign = row.LowSign;
f.Low = row.Low;
f.HighSign = row.HighSign;
f.High = row.High;
f.Display = row.Display;
f.Flag = row.Flag;
f.Interpretation = row.Interpretation;
f.Notes = row.Notes;
function handleRemove(id) { for (const key of ['AgeStart', 'AgeEnd']) {
tempNumeric = tempNumeric.filter((row) => row.id !== id); const val = row[key] ?? '';
if (editingId === id) resetForm(); const match = val.match(/(\d+)Y\s*(\d+)M\s*(\d+)D/);
} joinFields[key] = match
? { YY: match[1], MM: match[2], DD: match[3] }
: { DD: '', MM: '', YY: '' };
}
}
function getLabel(fieldKey, value) { function handleUpdate() {
const opts = props.refNumState.selectOptions[fieldKey] ?? []; tempNumeric = tempNumeric.map((row) =>
const found = opts.find((o) => o.value == value); row.id === editingId ? { id: row.id, ...snapshotForm() } : row
return found ? found.label : value; );
} resetForm();
}
function getCode(fieldKey, value) { function handleCancelEdit() {
const opts = props.refNumState.selectOptions[fieldKey] ?? []; resetForm();
const found = opts.find((o) => o.value == value); }
return found ? found.code : value;
}
const rangeTypeBadge = (type) => ({ REF: "REF", CRTC: "CRTC" }[type] ?? null); function handleRemove(id) {
const numRefTypeBadge = (type) => ({ RANGE: "R", THOLD: "T" }[type] ?? null);; tempNumeric = tempNumeric.filter((row) => row.id !== id);
if (editingId === id) resetForm();
}
const rangeDisplay = (row) => { function getLabel(fieldKey, value) {
if (row.NumRefType === "RANGE") return `${row.LowValue} - ${row.HighValue}`; const opts = props.refNumState.selectOptions[fieldKey] ?? [];
if (row.NumRefType === "THOLD") return row.TholdValue; const found = opts.find((o) => o.value == value);
return "-"; return found ? found.label : value;
}; }
$effect(() => { function getCode(fieldKey, value) {
for (const key of ["AgeStart", "AgeEnd"]) { const opts = props.refNumState.selectOptions[fieldKey] ?? [];
props.refNumState.form[key] = const found = opts.find((o) => o.value == value);
buildAgeText(joinFields[key]); return found ? found.code : value;
} }
});
$effect(() => { const rangeTypeBadge = (type) => ({ REF: 'REF', CRTC: 'CRTC' })[type] ?? null;
const allColumns = props.refNumFormFields.flatMap( const numRefTypeBadge = (type) => ({ RANGE: 'R', THOLD: 'T' })[type] ?? null;
(section) => section.rows.flatMap(
(row) => row.columns ?? []
)
);
untrack(() => {
for (const col of allColumns) {
if (!col.optionsEndpoint) continue;
props.refNumState.fetchOptions?.(
{
key: col.key,
optionsEndpoint: col.optionsEndpoint,
valueKey: col.valueKey,
labelKey: col.labelKey,
},
props.refNumState.form
);
}
})
});
$effect(() => { const rangeDisplay = (row) => {
if (!props.refNumState.form.Low || props.refNumState.form.Low === "") { if (row.NumRefType === 'RANGE') return `${row.LowValue} - ${row.HighValue}`;
props.refNumState.form.LowSign = ""; if (row.NumRefType === 'THOLD') return row.TholdValue;
} return '-';
};
if (!props.refNumState.form.High || props.refNumState.form.High === "") {
props.refNumState.form.HighSign = ""; $effect(() => {
} for (const key of ['AgeStart', 'AgeEnd']) {
}); props.refNumState.form[key] = buildAgeText(joinFields[key]);
}
});
$effect(() => {
const allColumns = props.refNumFormFields.flatMap((section) =>
section.rows.flatMap((row) => row.columns ?? [])
);
untrack(() => {
for (const col of allColumns) {
if (!col.optionsEndpoint) continue;
props.refNumState.fetchOptions?.(
{
key: col.key,
optionsEndpoint: col.optionsEndpoint,
valueKey: col.valueKey,
labelKey: col.labelKey
},
props.refNumState.form
);
}
});
});
$effect(() => {
if (!props.refNumState.form.Low || props.refNumState.form.Low === '') {
props.refNumState.form.LowSign = '';
}
if (!props.refNumState.form.High || props.refNumState.form.High === '') {
props.refNumState.form.HighSign = '';
}
});
</script> </script>
<div class="flex flex-col gap-4 w-full"> <div class="flex flex-col gap-4 w-full">
<div> <div>
<DictionaryFormRenderer <DictionaryFormRenderer
formState={props.refNumState} formState={props.refNumState}
formFields={props.refNumFormFields} formFields={props.refNumFormFields}
{disabledSign} {disabledSign}
bind:joinFields bind:joinFields
/> />
<div class="flex gap-2 mt-1 ms-2"> <div class="flex gap-2 mt-1 ms-2">
{#if editingId !== null} {#if editingId !== null}
<Button size="sm" class="cursor-pointer" onclick={handleUpdate}> <Button size="sm" class="cursor-pointer" onclick={handleUpdate}>Update</Button>
Update <Button size="sm" variant="outline" class="cursor-pointer" onclick={handleCancelEdit}>
</Button> Cancel
<Button size="sm" variant="outline" class="cursor-pointer" onclick={handleCancelEdit}> </Button>
Cancel {:else}
</Button> <Button size="sm" class="cursor-pointer" onclick={handleInsert}>Insert</Button>
{:else} {/if}
<Button size="sm" class="cursor-pointer" onclick={handleInsert}> </div>
Insert </div>
</Button>
{/if}
</div>
</div>
<Separator /> <Separator />
<div> <div>
<Table.Root> <Table.Root>
<Table.Header> <Table.Header>
<Table.Row class="hover:bg-transparent"> <Table.Row class="hover:bg-transparent">
<Table.Head>Specimen Type</Table.Head> <Table.Head>Specimen Type</Table.Head>
<Table.Head>Sex</Table.Head> <Table.Head>Sex</Table.Head>
<Table.Head>Age Range</Table.Head> <Table.Head>Age Range</Table.Head>
<Table.Head>Type</Table.Head> <Table.Head>Type</Table.Head>
<Table.Head>Range/Threshold</Table.Head> <Table.Head>Range/Threshold</Table.Head>
<Table.Head>Flag</Table.Head> <Table.Head>Flag</Table.Head>
<Table.Head>Interpretation</Table.Head> <Table.Head>Interpretation</Table.Head>
<Table.Head>Notes</Table.Head> <Table.Head>Notes</Table.Head>
<Table.Head class="w-[80px]"></Table.Head> <Table.Head class="w-[80px]"></Table.Head>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{#if tempNumeric.length === 0} {#if tempNumeric.length === 0}
<Table.Row> <Table.Row>
<Table.Cell colspan={9} class="text-center text-muted-foreground py-6"> <Table.Cell colspan={9} class="text-center text-muted-foreground py-6">
No data. Fill the form above and click Insert. No data. Fill the form above and click Insert.
</Table.Cell> </Table.Cell>
</Table.Row> </Table.Row>
{:else} {:else}
{#each tempNumeric as row (row.id)} {#each tempNumeric as row (row.id)}
<Table.Row <Table.Row class="cursor-pointer hover:bg-muted/50">
class="cursor-pointer hover:bg-muted/50" <Table.Cell>{row.SpcType ? getLabel('SpcType', row.SpcType) : '-'}</Table.Cell>
> <Table.Cell class="font-medium">{row.Sex ? getLabel('Sex', row.Sex) : '-'}</Table.Cell
<Table.Cell>{row.SpcType ? getLabel("SpcType", row.SpcType) : "-"}</Table.Cell> >
<Table.Cell class="font-medium">{row.Sex ? getLabel("Sex", row.Sex) : "-"}</Table.Cell> <Table.Cell>{row.AgeStart} &ndash; {row.AgeEnd}</Table.Cell>
<Table.Cell>{row.AgeStart} &ndash; {row.AgeEnd}</Table.Cell> <Table.Cell>
<Table.Cell> {#if rangeTypeBadge(row.RangeType)}
{#if rangeTypeBadge(row.RangeType)} <Badge>{rangeTypeBadge(row.RangeType)}</Badge>
<Badge>{rangeTypeBadge(row.RangeType)}</Badge> {:else}
{:else} -
- {/if}
{/if} </Table.Cell>
</Table.Cell> <Table.Cell class="font-medium flex justify-between">
<Table.Cell class="font-medium flex justify-between"> <div>
<div> {row.LowSign ? row.LowSign : ''}
{row.LowSign ? row.LowSign : ""} {row.Low || "null"} &ndash; {row.Low || 'null'} &ndash;
{row.HighSign ? row.HighSign : ""} {row.High || "null"} {row.HighSign ? row.HighSign : ''}
</div> {row.High || 'null'}
<Badge variant="outline" class="border-dashed border-primary border-2">{numRefTypeBadge(row.NumRefType)}</Badge> </div>
</Table.Cell> <Badge variant="outline" class="border-dashed border-primary border-2"
<Table.Cell class="font-medium">{row.Flag}</Table.Cell> >{numRefTypeBadge(row.NumRefType)}</Badge
<Table.Cell class="font-medium">{row.Interpretation}</Table.Cell> >
<Table.Cell class="font-medium">{row.Notes}</Table.Cell> </Table.Cell>
<Table.Cell> <Table.Cell class="font-medium">{row.Flag}</Table.Cell>
<div class="flex gap-1"> <Table.Cell class="font-medium">{row.Interpretation}</Table.Cell>
<Button <Table.Cell class="font-medium">{row.Notes}</Table.Cell>
size="icon" <Table.Cell>
variant="ghost" <div class="flex gap-1">
class="h-7 w-7 cursor-pointer" <Button
onclick={() => handleEdit(row)} size="icon"
> variant="ghost"
<PencilIcon class="h-3.5 w-3.5" /> class="h-7 w-7 cursor-pointer"
</Button> onclick={() => handleEdit(row)}
<Button >
size="icon" <PencilIcon class="h-3.5 w-3.5" />
variant="ghost" </Button>
class="h-7 w-7 cursor-pointer" <Button
onclick={() => handleRemove(row.id)} size="icon"
> variant="ghost"
<Trash2Icon class="h-3.5 w-3.5" /> class="h-7 w-7 cursor-pointer"
</Button> onclick={() => handleRemove(row.id)}
</div> >
</Table.Cell> <Trash2Icon class="h-3.5 w-3.5" />
</Table.Row> </Button>
{/each} </div>
{/if} </Table.Cell>
</Table.Body> </Table.Row>
</Table.Root> {/each}
</div> {/if}
</div> </Table.Body>
</Table.Root>
</div>
</div>

View File

@ -10,9 +10,8 @@
import { untrack } from "svelte"; import { untrack } from "svelte";
import { toDays } from "$lib/utils/ageUtils"; import { toDays } from "$lib/utils/ageUtils";
let { resetRefTxt = $bindable(), ...props } = $props() let { tempTxt = $bindable([]), resetRefTxt = $bindable(), ...props } = $props()
let tempTxt = $state([]);
let editingId = $state(null); let editingId = $state(null);
let idCounter = $state(0); let idCounter = $state(0);

View File

@ -15,6 +15,8 @@
import MoveRightIcon from '@lucide/svelte/icons/move-right'; import MoveRightIcon from '@lucide/svelte/icons/move-right';
import BrushCleaningIcon from '@lucide/svelte/icons/brush-cleaning'; import BrushCleaningIcon from '@lucide/svelte/icons/brush-cleaning';
import DeleteIcon from '@lucide/svelte/icons/delete'; import DeleteIcon from '@lucide/svelte/icons/delete';
import Trash2Icon from '@lucide/svelte/icons/trash-2';
import PlusIcon from '@lucide/svelte/icons/plus';
let { let {
formState, formState,
@ -27,7 +29,10 @@
hiddenFields, hiddenFields,
handleTestTypeChange, handleTestTypeChange,
handleResultTypeChange, handleResultTypeChange,
handleRefTypeChange handleRefTypeChange,
members = [],
onAddMember,
onRemoveMember,
} = $props(); } = $props();
const operators = ['+', '-', '*', '/', '^', '(', ')']; const operators = ['+', '-', '*', '/', '^', '(', ')'];
@ -610,6 +615,69 @@
</div> </div>
{/if} {/if}
</div> </div>
{:else if type === "members"}
{@const filteredOptions = getFilteredOptions(key)}
<div class="flex flex-col gap-2 w-full">
{#each members as member, index (member.id)}
{@const selectedLabel =
formState.selectOptions?.[key]?.find(
(opt) => opt.value === member.value
)?.label || 'Choose'}
<div class="flex gap-1 w-full">
<Button type="button" variant="outline" size="icon" disabled>
{index + 1}
</Button>
<div class="flex-1">
<Select.Root
type="single"
bind:value={member.value}
onOpenChange={(open) => {
if (open && optionsEndpoint) {
formState.fetchOptions?.(
{ key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey },
formState.form
);
}
}}
>
<Select.Trigger class="w-full">
{selectedLabel}
</Select.Trigger>
<Select.Content>
<div class="p-2">
<input
type="text"
placeholder="Search..."
class="w-full border rounded px-2 py-1 text-sm"
bind:value={searchQuery[key]}
/>
</div>
{#if formState.loadingOptions?.[key]}
<Select.Item disabled value="loading">Loading...</Select.Item>
{:else}
{#if !required}
<Select.Item value="">- None -</Select.Item>
{/if}
{#each filteredOptions as option}
<Select.Item value={option.value}>
{option.label}
</Select.Item>
{/each}
{/if}
</Select.Content>
</Select.Root>
</div>
<Button type="button" variant="outline" size="icon" onclick={() => onRemoveMember(member.id)}>
<Trash2Icon class="size-4" />
</Button>
</div>
{/each}
<Button variant="outline" onclick={onAddMember}>
<PlusIcon class="size-4" />
Add Test
</Button>
</div>
{:else} {:else}
<Input <Input
type="text" type="text"