working on initial testmap

This commit is contained in:
faiztyanirh 2026-02-25 16:42:14 +07:00
parent 7fcb5d8507
commit 44c9f29f09
6 changed files with 312 additions and 29 deletions

View File

@ -46,7 +46,6 @@ export const testCalSchema = z.object({
export const refNumSchema = z.object({
AgeStart: z.string().optional(),
AgeEnd: z.string().optional(),
Flag: z.string().min(1, "Required"),
Low: z.string().optional(),
High: z.string().optional(),
LowSign: z.string().optional(),
@ -94,6 +93,13 @@ export const refNumSchema = z.object({
path: ["LowSign"],
});
}
if (data.High && !data.HighSign) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Math sign for High is required",
path: ["HighSign"],
});
}
if (
data.LowSign &&
data.HighSign &&
@ -108,6 +114,29 @@ export const refNumSchema = z.object({
}
})
export const refTxtSchema = z.object({
AgeStart: z.string().optional(),
AgeEnd: z.string().optional(),
})
.superRefine((data, ctx) => {
const start = toDays(data.AgeStart);
const end = toDays(data.AgeEnd);
// if (start !== null && start > 0 && end !== null && end <= start) {
if (start !== null && end !== null && end <= start) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Age End must be greater than Age Start",
path: ["AgeEnd"],
});
}
})
export const testMapSchema = z.object({
HostID: z.string().optional(),
ClientID: z.string().optional(),
})
export const testInitialForm = {
TestSiteID: "",
SiteID: "",
@ -180,6 +209,19 @@ export const refTxtInitialForm = {
Notes: "",
};
export const testMapInitialForm = {
TestMapID: "",
HostType: "",
HostID: "",
HostTestCode: "",
HostTestName: "",
ClientType: "",
ClientID: "",
ClientTestCode: "",
ClientTestName: "",
ConDefID: "",
}
export const testDefaultErrors = {
TestSiteCode: "Required",
TestSiteName: "Required",
@ -198,7 +240,15 @@ export const refNumDefaultErrors = {
HighSign: null,
};
export const refTxtDefaultErrors = {};
export const refTxtDefaultErrors = {
AgeStart: null,
AgeEnd: null,
};
export const testMapDefaultErrors = {
HostID: null,
ClientID: null,
}
export const testFormFields = [
{
@ -699,12 +749,14 @@ export const refTxtFormFields = [
label: "Age Start",
required: false,
type: "agejoin",
validateOn: ["input"]
},
{
key: "AgeEnd",
label: "Age End",
required: false,
type: "agejoin",
validateOn: ["input"]
},
]
},
@ -768,6 +820,99 @@ 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",
},
{
key: "ClientID",
label: "Client ID",
required: false,
type: "text",
}
]
},
{
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 [
{

View File

@ -6,19 +6,25 @@
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, refTxtInitialForm, refTxtFormFields } from "$lib/components/dictionary/test/config/test-form-config";
import { buildTestPayload,
testCalSchema, testCalInitialForm, testCalDefaultErrors, testCalFormFields,
refNumSchema, refNumDefaultErrors, refNumInitialForm, refNumFormFields,
refTxtSchema, refTxtDefaultErrors, refTxtInitialForm, refTxtFormFields,
testMapSchema, testMapInitialForm, testMapDefaultErrors, testMapFormFields
} 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 { API } from "$lib/config/api";
import { untrack } from "svelte";
let props = $props();
let resetRefNum = $state();
let resetRefTxt = $state();
let resetMap = $state();
const { masterDetail, formFields, formActions, schema, initialForm } = props.context;
@ -36,10 +42,18 @@
});
const refTxtState = useForm({
schema: null,
schema: refTxtSchema,
initialForm: refTxtInitialForm,
defaultErrors: refTxtDefaultErrors,
});
const mapFormState = useForm({
schema: testMapSchema,
initialForm: testMapInitialForm,
defaultErrors: testMapDefaultErrors,
modeOpt: 'cascade'
})
const helpers = useDictionaryForm(formState);
const handlers = {
@ -329,7 +343,7 @@
group
</Tabs.Content>
<Tabs.Content value="map">
map
<Map {mapFormState} {testMapFormFields} bind:resetMap={resetMap}/>
</Tabs.Content>
<Tabs.Content value="reference">
<div class="w-full h-full flex items-start">

View File

@ -0,0 +1,105 @@
<script>
import DictionaryFormRenderer from "$lib/components/reusable/form/dictionary-form-renderer.svelte";
import { Separator } from "$lib/components/ui/separator/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import PencilIcon from "@lucide/svelte/icons/pencil";
import Trash2Icon from "@lucide/svelte/icons/trash-2";
let { resetMap = $bindable(), ...props } = $props()
let tempMap = $state([]);
let editingId = $state(null);
let idCounter = $state(0);
resetMap = () => {
tempMap = [];
};
</script>
<div class="flex flex-col gap-4 w-full">
<div>
<DictionaryFormRenderer
formState={props.mapFormState}
formFields={props.testMapFormFields}
/>
<div class="flex gap-2 mt-1 ms-2">
{#if editingId !== null}
<Button size="sm" class="cursor-pointer">
Update
</Button>
<Button size="sm" variant="outline" class="cursor-pointer">
Cancel
</Button>
{:else}
<Button size="sm" class="cursor-pointer">
Insert
</Button>
{/if}
</div>
</div>
<Separator />
<div>
<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>
<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.HostType}</Table.Cell>
<Table.Cell>{row.HostID}</Table.Cell>
<Table.Cell>{row.HostTestCode}</Table.Cell>
<Table.Cell>{row.HostTestName}</Table.Cell>
<Table.Cell>{row.ClientType}</Table.Cell>
<Table.Cell>{row.ClientID}</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"
>
<PencilIcon class="h-3.5 w-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
class="h-7 w-7 cursor-pointer"
>
<Trash2Icon class="h-3.5 w-3.5" />
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body>
</Table.Root>
</div>
</div>

View File

@ -8,6 +8,7 @@
import PencilIcon from "@lucide/svelte/icons/pencil";
import Trash2Icon from "@lucide/svelte/icons/trash-2";
import { untrack } from "svelte";
import { toDays } from "$lib/utils/ageUtils";
let { resetRefNum = $bindable(), ...props } = $props()
@ -62,28 +63,28 @@
editingId = null;
}
function isOverlapping(newLow, newHigh, existingRows) {
return existingRows.some(row => {
return !(newHigh <= row.Low || newLow >= row.High);
});
}
function handleInsert() {
const low = Number(props.refNumState.form.Low);
const high = Number(props.refNumState.form.High);
console.log(`low: ${low}`);
// 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 => {
const existingLow = Number(row.Low);
const existingHigh = Number(row.High);
return !(high < existingLow || low > existingHigh);
const isOverlap = tempNumeric.some(row => {
const existingStart = toDays(row.AgeStart);
const existingEnd = toDays(row.AgeEnd);
if (existingStart == null || existingEnd == null) return false;
return !(newEnd < existingStart || newStart > existingEnd);
});
if (isOverlap) {
props.refNumState.errors.High = "Range overlaps with existing data";
props.refNumState.errors.AgeEnd =
"Age range overlaps with existing data";
return;
}
@ -194,12 +195,10 @@
});
$effect(() => {
// Sinkronisasi Low dan LowSign
if (!props.refNumState.form.Low || props.refNumState.form.Low === "") {
props.refNumState.form.LowSign = "";
}
// Sinkronisasi High dan HighSign
if (!props.refNumState.form.High || props.refNumState.form.High === "") {
props.refNumState.form.HighSign = "";
}

View File

@ -8,6 +8,7 @@
import PencilIcon from "@lucide/svelte/icons/pencil";
import Trash2Icon from "@lucide/svelte/icons/trash-2";
import { untrack } from "svelte";
import { toDays } from "$lib/utils/ageUtils";
let { resetRefTxt = $bindable(), ...props } = $props()
@ -48,8 +49,31 @@
}
function handleInsert() {
const row = { id: ++idCounter, ...snapshotForm() };
const newStart = toDays(props.refTxtState.form.AgeStart);
const newEnd = toDays(props.refTxtState.form.AgeEnd);
const isOverlap = tempTxt.some(row => {
const existingStart = toDays(row.AgeStart);
const existingEnd = toDays(row.AgeEnd);
if (existingStart == null || existingEnd == null) return false;
return !(newEnd < existingStart || newStart > existingEnd);
});
if (isOverlap) {
props.refTxtState.errors.AgeEnd =
"Age range overlaps with existing data";
return;
}
const row = {
id: ++idCounter,
...snapshotForm()
};
tempTxt = [...tempTxt, row];
resetForm();
}
@ -105,12 +129,6 @@
const txtRefTypeBadge = (type) => ({ TEXT: "TX", VSET: "VS" }[type] ?? null);;
// $effect(() => {
// if (props.refType) {
// props.refTxtState.form.TxtRefType = props.refType;
// }
// });
$effect(() => {
for (const key of ["AgeStart", "AgeEnd"]) {
props.refTxtState.form[key] =

View File

@ -236,6 +236,7 @@
formState.validateField("Low", formState.form[txtKey]);
formState.validateField("LowSign");
formState.validateField("High", formState.form[txtKey]);
formState.validateField("HighSign");
}
}}
/>
@ -304,6 +305,7 @@
<InputGroup.Input type="number" bind:value={joinFields[key].YY}
oninput={() => {
if (validateOn?.includes("input")) {
console.log('object');
// formState.validateField(key, formState.form[key], false);
formState.validateField("AgeStart");
formState.validateField("AgeEnd");