diff --git a/src/lib/components/composable/use-form-validation.svelte.js b/src/lib/components/composable/use-form-validation.svelte.js index 3330c31..2945d53 100644 --- a/src/lib/components/composable/use-form-validation.svelte.js +++ b/src/lib/components/composable/use-form-validation.svelte.js @@ -1,27 +1,60 @@ const validationMode = { - create: (schema, field, value) => { - const result = schema.shape[field].safeParse(value); - return result.success ? null : result.error.issues[0].message; + // create: (schema, field, value) => { + // const result = schema.shape[field].safeParse(value); + // console.log(result); + // return result.success ? null : result.error.issues[0].message; + // }, + + create: (schema, field, form) => { + const result = schema.safeParse(form); + + if (result.success) return null; + + const fieldError = result.error.issues.find( + (issue) => issue.path[0] === field + ); + + return fieldError ? fieldError.message : null; }, - edit: (schema, field, value, originalValue) => { - if (originalValue !== undefined && value === originalValue) { + // edit: (schema, field, value, originalValue) => { + // if (originalValue !== undefined && value === originalValue) { + // return null; + // } + + // const result = schema.shape[field].safeParse(value); + // return result.success ? null : result.error.issues[0].message; + // } + edit: (schema, field, form, originalValue) => { + if (originalValue !== undefined && form[field] === originalValue) { return null; } - const result = schema.shape[field].safeParse(value); - return result.success ? null : result.error.issues[0].message; + const result = schema.safeParse(form); + + if (result.success) return null; + + const fieldError = result.error.issues.find( + (issue) => issue.path[0] === field + ); + + return fieldError ? fieldError.message : null; } }; export function useFormValidation(schema, form, defaultErrors, valMode) { const errors = $state({...defaultErrors}) + // function validateField(field, originalValue) { + // const value = form[field]; + // const valFn = validationMode[valMode]; + // errors[field] = valFn(schema, field, value, originalValue); + // } function validateField(field, originalValue) { - const value = form[field]; - const valFn = validationMode[valMode]; - errors[field] = valFn(schema, field, value, originalValue); - } + const valFn = validationMode[valMode]; + const error = valFn(schema, field, form, originalValue); + errors[field] = error; + } function resetErrors() { Object.assign(errors, defaultErrors); diff --git a/src/lib/components/dictionary/test/config/test-form-config.js b/src/lib/components/dictionary/test/config/test-form-config.js index fb9a74d..b864d89 100644 --- a/src/lib/components/dictionary/test/config/test-form-config.js +++ b/src/lib/components/dictionary/test/config/test-form-config.js @@ -2,6 +2,7 @@ import { API } from "$lib/config/api"; import EraserIcon from "@lucide/svelte/icons/eraser"; import { z } from "zod"; import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings"; +import { toDays } from "$lib/utils/ageUtils"; export const testSchema = z.object({ TestSiteCode: z.string().min(1, "Required"), @@ -13,7 +14,7 @@ export const testSchema = z.object({ return Number(val); }, z.number({invalid_type_error: "Must be a number"}).min(0, "Min 0").max(7, "Max 7").optional() - ) + ), }).superRefine((data, ctx) => { if (data.Factor && !data.Unit2) { ctx.addIssue({ @@ -42,6 +43,71 @@ 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(), + HighSign: z.string().optional(), + NumRefType: 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"], + }); + } +}) +.superRefine((data, ctx) => { + // console.log(data.NumRefType); + if (data.NumRefType === "RANGE") { + const low = Number(data.Low); + const high = Number(data.High); + + if ( + data.Low && + data.High && + !Number.isNaN(low) && + !Number.isNaN(high) && + (high <= low) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "High value must be greater than Low value", + path: ["High"], + }); + } + } + if (data.NumRefType === "THOLD") { + if (data.Low && !data.LowSign) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Math sign for Low is required", + path: ["LowSign"], + }); + } + if ( + data.LowSign && + data.HighSign && + data.LowSign === data.HighSign + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "High sign can't be the same as Low sign", + path: ["HighSign"], + }); + } + } +}) + export const testInitialForm = { TestSiteID: "", SiteID: "", @@ -123,7 +189,14 @@ export const testDefaultErrors = { export const testCalDefaultErrors = {}; -export const refNumDefaultErrors = {}; +export const refNumDefaultErrors = { + AgeStart: null, + AgeEnd: null, + Low: null, + High: null, + LowSign: null, + HighSign: null, +}; export const refTxtDefaultErrors = {}; @@ -482,12 +555,14 @@ export const refNumFormFields = [ label: "Age Start", required: false, type: "agejoin", + validateOn: ["input"] }, { key: "AgeEnd", label: "Age End", required: false, type: "agejoin", + validateOn: ["input"] }, ] }, @@ -524,6 +599,7 @@ export const refNumFormFields = [ type: "signvalue", txtKey: "Low", optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/math_sign`, + validateOn: ["input"] }, { key: "HighSign", @@ -532,6 +608,7 @@ export const refNumFormFields = [ type: "signvalue", txtKey: "High", optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/math_sign`, + validateOn: ["input"] }, ] }, diff --git a/src/lib/components/dictionary/test/page/create-page.svelte b/src/lib/components/dictionary/test/page/create-page.svelte index fd1ac37..677b56a 100644 --- a/src/lib/components/dictionary/test/page/create-page.svelte +++ b/src/lib/components/dictionary/test/page/create-page.svelte @@ -6,7 +6,7 @@ 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, refNumInitialForm, refNumFormFields, refTxtInitialForm, refTxtFormFields } from "$lib/components/dictionary/test/config/test-form-config"; + import { buildTestPayload, testCalSchema, testCalInitialForm, testCalDefaultErrors, testCalFormFields, refNumSchema, refNumDefaultErrors, refNumInitialForm, refNumFormFields, refTxtInitialForm, refTxtFormFields } 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"; @@ -30,8 +30,9 @@ }); const refNumState = useForm({ - schema: null, + schema: refNumSchema, initialForm: refNumInitialForm, + defaultErrors: refNumDefaultErrors, }); const refTxtState = useForm({ diff --git a/src/lib/components/dictionary/test/page/tabs/ref-num.svelte b/src/lib/components/dictionary/test/page/tabs/ref-num.svelte index 79b8f15..70d2e62 100644 --- a/src/lib/components/dictionary/test/page/tabs/ref-num.svelte +++ b/src/lib/components/dictionary/test/page/tabs/ref-num.svelte @@ -25,14 +25,14 @@ }); let disabledSign = $derived.by(() => { - const refType = props.refType; + 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 { @@ -62,9 +62,38 @@ editingId = null; } + function isOverlapping(newLow, newHigh, existingRows) { + return existingRows.some(row => { + return !(newHigh <= row.Low || newLow >= row.High); + }); + } + function handleInsert() { - const row = { id: ++idCounter, ...snapshotForm() }; + const low = Number(props.refNumState.form.Low); + const high = Number(props.refNumState.form.High); + console.log(`low: ${low}`); + // 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); + }); + + if (isOverlap) { + props.refNumState.errors.High = "Range overlaps with existing data"; + return; + } + + const row = { + id: ++idCounter, + ...snapshotForm() + }; + tempNumeric = [...tempNumeric, row]; + resetForm(); } @@ -163,6 +192,18 @@ } }) }); + + $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 = ""; + } + });
@@ -230,8 +271,8 @@
- {row.LowSign ? row.LowSign : ""} {row.Low || ""} – - {row.HighSign ? row.HighSign : ""} {row.High || ""} + {row.LowSign ? row.LowSign : ""} {row.Low || "null"} – + {row.HighSign ? row.HighSign : ""} {row.High || "null"}
{numRefTypeBadge(row.NumRefType)}
diff --git a/src/lib/components/reusable/form/dictionary-form-renderer.svelte b/src/lib/components/reusable/form/dictionary-form-renderer.svelte index 57d3127..cc728a5 100644 --- a/src/lib/components/reusable/form/dictionary-form-renderer.svelte +++ b/src/lib/components/reusable/form/dictionary-form-renderer.svelte @@ -92,6 +92,7 @@ type="text" bind:value={formState.form[key]} oninput={() => { + // console.log(`key: ${key}, form: ${formState.form[key]}`); if (validateOn?.includes("input")) { formState.validateField(key, formState.form[key], false); } @@ -215,79 +216,6 @@ {/if} - {:else if type === "date"} - {:else if type === "signvalue"} + {:else if type === "signvalue"} { + if (validateOn?.includes("input")) { + formState.validateField("Low", formState.form[txtKey]); + formState.validateField("LowSign"); + formState.validateField("High", formState.form[txtKey]); + } + }} /> - {formState.selectOptions?.[key]?.find( - opt => opt.value === formState.form[key] - )?.label || 'Choose'} + {formState.selectOptions?.[key]?.find(opt => opt.value === formState.form[key])?.label || 'Choose'} {/snippet} @@ -335,12 +268,27 @@ Loading... {:else} + { + formState.form[key] = ""; + dropdownOpen[key] = false; + formState.validateField("LowSign"); + formState.validateField("HighSign"); + }} + > + - None - + {#each formState.selectOptions?.[key] || [] as option} { formState.form[key] = option.value; dropdownOpen[key] = false; }} + onSelect={() => { + formState.form[key] = option.value; + formState.validateField("LowSign"); + formState.validateField("HighSign"); + }} > {option.label} @@ -353,19 +301,43 @@ {:else if type === "agejoin"}
- + { + if (validateOn?.includes("input")) { + // formState.validateField(key, formState.form[key], false); + formState.validateField("AgeStart"); + formState.validateField("AgeEnd"); + } + }} + /> Year - + { + if (validateOn?.includes("input")) { + // formState.validateField(key, formState.form[key], false); + formState.validateField("AgeStart"); + formState.validateField("AgeEnd"); + } + }} + /> Month - + { + if (validateOn?.includes("input")) { + // formState.validateField(key, formState.form[key], false); + formState.validateField("AgeStart"); + formState.validateField("AgeEnd"); + } + }} + /> Day @@ -388,9 +360,9 @@ {formState.errors[key]} {/if} --> - {#if formState.errors[key]} + {#if formState.errors[key] || formState.errors[txtKey]} - {formState.errors[key]} + {formState.errors[key] ?? formState.errors[txtKey]}