25/03/2026

multiple fix/add :
change testdefsite to use isBoolean
add toggle components in renderer
change payload agestart ageend to days
initial edit form
This commit is contained in:
faiztyanirh 2026-03-25 16:12:16 +07:00
parent 5cab6097a9
commit 1389eac272
10 changed files with 487 additions and 84 deletions

View File

@ -112,10 +112,10 @@ export async function create(endpoint, formData) {
}
}
export async function update(endpoint, formData) {
export async function update(endpoint, formData, id) {
console.log(cleanEmptyStrings(formData));
try {
const res = await fetch(`${API.BASE_URL}${endpoint}`, {
const res = await fetch(`${API.BASE_URL}${endpoint}/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',

View File

@ -2,7 +2,7 @@ import { useFormState } from "./use-form-state.svelte";
import { useFormOptions } from "./use-form-option.svelte";
import { useFormValidation } from "./use-form-validation.svelte";
export function useForm({schema, initialForm, defaultErrors = {}, mode = 'create', modeOpt = 'default', saveEndpoint = null, editEndpoint = null}) {
export function useForm({schema, initialForm, defaultErrors = {}, mode = 'create', modeOpt = 'default', saveEndpoint = null, editEndpoint = null, idKey = null}) {
const state = useFormState(initialForm);
const val = useFormValidation(schema, state.form, defaultErrors, mode);
const options = useFormOptions(modeOpt);
@ -13,9 +13,16 @@ export function useForm({schema, initialForm, defaultErrors = {}, mode = 'create
try {
// const payload = { ...state.form };
const payload = customPayload || { ...state.form };
let result;
// const { ProvinceID, CityID, ...rest } = state.form;
// const payload = customPayload || rest;
const result = currentMode === 'edit' ? await editEndpoint(payload) : await saveEndpoint(payload);
// const result = currentMode === 'edit' ? await editEndpoint(payload, idKey) : await saveEndpoint(payload);
if (currentMode === 'edit') {
const id = payload[idKey];
result = await editEndpoint(payload, id);
} else {
result = await saveEndpoint(payload);
}
return result;
} catch (error) {
console.error('Save failed', error);

View File

@ -13,6 +13,6 @@ export async function createTest(newTestForm) {
return await create(API.TEST, newTestForm)
}
export async function editTest(editTestForm) {
return await update(API.TEST, editTestForm)
export async function editTest(editTestForm, id) {
return await update(API.TEST, editTestForm, id)
}

View File

@ -227,10 +227,11 @@ export const testInitialForm = {
ExpectedTAT: '',
SeqScr: '',
SeqRpt: '',
VisibleScr: '',
VisibleRpt: '',
CountStat: '',
Level: ''
isVisibleScr: 1,
isVisibleRpt: 1,
isCountStat: 1,
Level: '',
isRequestable: 1,
};
export const testCalInitialForm = {
@ -464,7 +465,7 @@ export const testFormFields = [
key: 'Factor',
label: 'Factor',
required: false,
type: 'text'
type: 'number'
}
]
},
@ -540,7 +541,7 @@ export const testFormFields = [
]
},
{
title: 'Display Settings',
title: 'Display & Other Settings',
rows: [
{
type: 'row',
@ -552,10 +553,35 @@ export const testFormFields = [
type: 'text'
},
{
key: 'VisibleScr',
label: 'Visible on Screen',
key: 'SeqRpt',
label: 'Sequence on Report',
required: false,
type: 'text'
},
]
},
{
type: 'row',
columns: [
{
key: 'isVisibleScr',
label: 'Visible on Screen',
required: false,
type: 'toggle',
optionsToggle: [
{ value: 0, label: 'Disabled' },
{ value: 1, label: 'Enabled' },
],
},
{
key: 'isVisibleRpt',
label: 'Visible on Report',
required: false,
type: 'toggle',
optionsToggle: [
{ value: 0, label: 'Disabled' },
{ value: 1, label: 'Enabled' },
],
}
]
},
@ -563,43 +589,30 @@ export const testFormFields = [
type: 'row',
columns: [
{
key: 'SeqRpt',
label: 'Sequence on Report',
required: false,
type: 'text'
},
{
key: 'VisibleRpt',
label: 'Visible on Report',
required: false,
type: 'text'
}
]
}
]
},
{
title: 'Other Settings',
rows: [
{
type: 'row',
columns: [
{
key: 'CountStat',
key: 'isCountStat',
label: 'Statistic',
required: false,
type: 'text'
type: 'toggle',
optionsToggle: [
{ value: 0, label: 'Disabled' },
{ value: 1, label: 'Enabled' },
],
},
{
key: 'Level',
label: 'Level',
key: 'isRequestable',
label: 'Requestable',
required: false,
type: 'text'
}
type: 'toggle',
optionsToggle: [
{ value: 0, label: 'Disabled' },
{ value: 1, label: 'Enabled' },
],
fullWidth: false
},
]
}
},
]
}
},
];
export const testCalFormFields = [
@ -1031,16 +1044,26 @@ export function buildTestPayload({ mainForm, activeFormStates, testType, refNumD
const state = activeFormStates[key];
if (key === 'refNum' && refNumData?.length > 0) {
payload[key] = refNumData;
// payload[key] = refNumData;
payload.refnum = refNumData.map(row => ({
...row,
AgeStart: toDays(row.AgeStart),
AgeEnd: toDays(row.AgeEnd),
}))
} else if (key === 'refTxt' && refTxtData?.length > 0) {
payload[key] = refTxtData;
// payload[key] = refTxtData;
payload.refTxt = refTxtData.map(row => ({
...row,
AgeStart: toDays(row.AgeStart),
AgeEnd: toDays(row.AgeEnd),
}))
} else if(key === 'group' && state.form?.Members?.length > 0) {
payload[key] = {
...state.form,
Members: state.form?.Members?.map((m) => m.value).filter(Boolean) ?? []
};
} else if (key === 'map' && mapData?.length > 0) {
payload[key] = mapData;
payload.testMap = mapData;
} else if (key === 'cal') {
payload[key] = {
...state.form

View File

@ -148,10 +148,10 @@
});
console.log(payload);
// const result = await formState.save(masterDetail.mode, payload);
const result = await formState.save(masterDetail.mode, payload);
// toast('Test Created!');
// masterDetail?.exitForm(true);
toast('Test Created!');
masterDetail?.exitForm(true);
}
const primaryAction = $derived({

View File

@ -3,40 +3,130 @@
import FormPageContainer from "$lib/components/reusable/form/form-page-container.svelte";
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte";
import { toast } from "svelte-sonner";
import { untrack } from "svelte";
import { API } from "$lib/config/api";
import ReusableAlertDialog from "$lib/components/reusable/reusable-alert-dialog.svelte";
import * as Tabs from '$lib/components/ui/tabs/index.js';
import { useForm } from '$lib/components/composable/use-form.svelte';
import {
buildTestPayload,
testCalSchema,
testCalInitialForm,
testCalDefaultErrors,
testCalFormFields,
refNumSchema,
refNumDefaultErrors,
refNumInitialForm,
refNumFormFields,
refTxtSchema,
refTxtDefaultErrors,
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 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;
const { formState } = masterDetail;
$inspect(formState.form)
const calFormState = useForm({
schema: testCalSchema,
initialForm: testCalInitialForm,
defaultErrors: testCalDefaultErrors
});
const groupFormState = useForm({
schema: testGroupSchema,
initialForm: testGroupInitialForm,
defaultErrors: testGroupDefaultErrors,
});
const refNumState = useForm({
schema: refNumSchema,
initialForm: refNumInitialForm,
defaultErrors: refNumDefaultErrors
});
const refTxtState = useForm({
schema: refTxtSchema,
initialForm: refTxtInitialForm,
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 ?? '';
let refState = {};
if (refType === 'RANGE' || refType === 'THOLD') {
refState = { refNum: refNumState };
} else if (refType === 'TEXT' || refType === 'VSET') {
refState = { refTxt: refTxtState };
}
switch (testType) {
case 'TEST':
case 'PARAM':
return {
...refState,
map: mapFormState
};
case 'CALC':
return {
cal: calFormState,
...refState,
map: mapFormState
};
case 'GROUP':
return {
group: groupFormState
}
case 'TITLE':
default:
return {};
}
});
const helpers = useDictionaryForm(formState);
const allColumns = formFields.flatMap((section) =>
section.rows.flatMap((row) => row.columns ?? [])
);
let showConfirm = $state(false);
$effect(() => {
untrack(() => {
formFields.forEach(group => {
group.rows.forEach(row => {
row.columns.forEach(col => {
if (col.type === "group") {
col.columns.forEach(child => {
if (child.type === "select" && child.optionsEndpoint) {
formState.fetchOptions(child, formState.form);
}
});
} else if ((col.type === "select") && col.optionsEndpoint) {
formState.fetchOptions(col, formState.form);
}
});
});
});
});
});
async function handleEdit() {
const result = await formState.save(masterDetail.mode);
@ -57,14 +147,265 @@
});
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(() => {
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(() => {
const resultType = formState?.form?.ResultType;
switch (resultType) {
case 'NMRIC':
return ['TEXT', 'VSET', 'NOREF'];
case 'RANGE':
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'];
}
});
let hiddenFields = $derived.by(() => {
const resultType = formState?.form?.ResultType;
return resultType !== 'VSET' ? ['VSet'] : [];
});
let refComponent = $derived.by(() => {
const refType = formState.form.RefType;
if (refType === 'RANGE' || refType === 'THOLD') return 'numeric';
if (refType === 'TEXT' || refType === 'VSET') return 'text';
return null;
});
const refTxtFormFieldsTransformed = $derived.by(() => {
return refTxtFormFields.map((group) => ({
...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) {
return {
...col,
type: 'textarea',
optionsEndpoint: undefined
};
}
return {
...col,
type: 'select',
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/${formState.form.VSet}`,
fullWidth: false
};
})
}))
}));
});
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');
$effect(() => {
const mainForm = formState.form;
if (mainForm.refnum && Array.isArray(mainForm.refnum)) {
refNumData = mainForm.refnum
}
})
$effect(() => {
untrack(() => {
formFields.forEach(group => {
group.rows.forEach(row => {
row.columns.forEach(col => {
if (col.type === "group") {
col.columns.forEach(child => {
if (child.type === "select" && child.optionsEndpoint) {
formState.fetchOptions(child, formState.form);
}
});
} else if ((col.type === "select") && col.optionsEndpoint) {
formState.fetchOptions(col, formState.form);
}
});
});
});
});
});
$effect(() => {
if (!availableTabs.includes(activeTab)) {
activeTab = availableTabs[0];
}
});
$effect(() => {
for (const key of hiddenFields) {
formState.form[key] = '';
}
});
</script>
<FormPageContainer title="Edit Test" {primaryAction} {secondaryActions}>
<DictionaryFormRenderer
{formState}
formFields={formFields}
mode="edit"
/>
<Tabs.Root bind:value={activeTab} class="w-full h-full">
<Tabs.List>
{#if availableTabs.includes('definition')}
<Tabs.Trigger value="definition">Definition</Tabs.Trigger>
{/if}
{#if availableTabs.includes('calculation')}
<Tabs.Trigger value="calculation">Calculation</Tabs.Trigger>
{/if}
{#if availableTabs.includes('group')}
<Tabs.Trigger value="group">Group</Tabs.Trigger>
{/if}
{#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
{formState}
formFields={formFields}
mode="edit"
{hiddenFields}
/>
</Tabs.Content>
<Tabs.Content value="calculation">
<Calculation {calFormState} {testCalFormFields} />
</Tabs.Content>
<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'}
<RefNum {refNumState} {refNumFormFields} bind:tempNumeric={refNumData} bind:resetRefNum />
{:else if refComponent === 'text'}
<RefTxt {refTxtState} refTxtFormFields={refTxtFormFieldsTransformed} bind:tempTxt={refTxtData} bind:resetRefTxt />
{:else}
<div class="h-full w-full flex items-center">
<ReusableEmpty desc="Select a Reference Type" />
</div>
{/if}
</div>
</Tabs.Content>
</Tabs.Root>
</FormPageContainer>
<ReusableAlertDialog

View File

@ -113,7 +113,7 @@
resetForm();
}
$inspect(props.refNumState.form)
function handleEdit(row) {
editingId = row.id;

View File

@ -19,6 +19,8 @@
import Trash2Icon from '@lucide/svelte/icons/trash-2';
import PlusIcon from '@lucide/svelte/icons/plus';
import CornerDownLeftIcon from '@lucide/svelte/icons/corner-down-left';
import CheckIcon from "@lucide/svelte/icons/check";
import XIcon from "@lucide/svelte/icons/x";
let {
formState,
@ -114,6 +116,7 @@
type,
optionsEndpoint,
options,
optionsToggle,
validateOn,
dependsOn,
endpointParamKey,
@ -266,11 +269,17 @@
}
}}
onOpenChange={(open) => {
if (open && optionsEndpoint) {
formState.fetchOptions?.(
{ key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey },
formState.form
);
if (open) {
if (options && options.length > 0) {
if (formState.selectOptions) {
formState.selectOptions[key] = options;
}
} else if (optionsEndpoint) {
formState.fetchOptions?.(
{ key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey },
formState.form
);
}
}
}}
>
@ -806,6 +815,28 @@
Add Test
</Button>
</div>
{:else if type === "toggle"}
{@const toggleOff = optionsToggle?.[0] ?? { value: false, label: 'Off' }}
{@const toggleOn = optionsToggle?.[1] ?? { value: true, label: 'On' }}
{@const isOn = String(formState.form[key]) === String(toggleOn.value)}
<div class="flex items-center w-full">
<Toggle
aria-label="Toggle"
variant="outline"
class="w-full transition-all data-[state=on]:text-primary"
pressed={isOn}
onPressedChange={(pressed) => {
formState.form[key] = pressed ? toggleOn.value : toggleOff.value;
}}
>
{#if isOn}
<CheckIcon class="mr-2 h-4 w-4" />
{:else}
<XIcon class="mr-2 h-4 w-4" />
{/if}
{isOn ? toggleOn.label : toggleOff.label}
</Toggle>
</div>
{:else}
<Input
type="text"

View File

@ -24,5 +24,5 @@ export const API = {
DISCIPLINE: '/api/organization/discipline',
DEPARTMENT: '/api/organization/department',
WORKSTATION: '/api/organization/workstation',
TEST: '/api/tests',
TEST: '/api/test',
};

View File

@ -20,6 +20,7 @@
modeOpt: 'default',
saveEndpoint: createTest,
editEndpoint: editTest,
idKey: 'TestSiteID',
}
});