mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-22 17:19:52 +07:00
melanjutkan pusing pusing validate rule refnum
This commit is contained in:
parent
62b1d9fe45
commit
7fcb5d8507
@ -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);
|
||||
|
||||
@ -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"]
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 = "";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
@ -230,8 +271,8 @@
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-medium flex justify-between">
|
||||
<div>
|
||||
{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"}
|
||||
</div>
|
||||
<Badge variant="outline" class="border-dashed border-primary border-2">{numRefTypeBadge(row.NumRefType)}</Badge>
|
||||
</Table.Cell>
|
||||
|
||||
@ -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}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<!-- {:else if type === "selectmultiple"}
|
||||
{@const filteredOptions = getFilteredOptions(key)}
|
||||
{@const selectedValues = Array.isArray(formState.form[key]) ? formState.form[key] : []}
|
||||
{@const selectedOptions = selectedValues
|
||||
.map(val => formState.selectOptions?.[key]?.find(opt => opt.value === val))
|
||||
.filter(Boolean)}
|
||||
|
||||
<Select.Root type="multiple" bind:value={formState.form[key]}
|
||||
onValueChange={(vals) => {
|
||||
formState.form[key] = vals;
|
||||
if (validateOn?.includes("input")) {
|
||||
formState.validateField?.(key, formState.form[key], false);
|
||||
}
|
||||
if (key === 'FormulaInput') {
|
||||
formState.validateField?.('FormulaCode', formState.form['FormulaCode'], false);
|
||||
}
|
||||
}}
|
||||
onOpenChange={(open) => {
|
||||
if (open && optionsEndpoint) {
|
||||
formState.fetchOptions?.(
|
||||
{ key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey },
|
||||
formState.form
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class="w-full min-h-[42px]">
|
||||
{#if selectedOptions.length === 0}
|
||||
<span class="text-muted-foreground">Choose</span>
|
||||
{:else}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each selectedOptions as option}
|
||||
<Badge variant="secondary" class="text-xs">
|
||||
{option.label}
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</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}
|
||||
disabled={key === "ResultType" && disabledResultTypes.includes(option.value) ||
|
||||
key === "RefType" && disabledReferenceTypes.includes(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
{/if}
|
||||
</Select.Content>
|
||||
{#if selectedValues.length > 0}
|
||||
<button
|
||||
class="w-full px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground"
|
||||
onclick={() => formState.form[key] = []}
|
||||
>
|
||||
Unselect All
|
||||
</button>
|
||||
{/if}
|
||||
</Select.Root> -->
|
||||
{:else if type === "date"}
|
||||
<ReusableCalendar
|
||||
bind:value={formState.form[key]}
|
||||
@ -298,11 +226,18 @@
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else if type === "signvalue"}
|
||||
{:else if type === "signvalue"}
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input
|
||||
placeholder="Type here"
|
||||
bind:value={formState.form[txtKey]}
|
||||
oninput={() => {
|
||||
if (validateOn?.includes("input")) {
|
||||
formState.validateField("Low", formState.form[txtKey]);
|
||||
formState.validateField("LowSign");
|
||||
formState.validateField("High", formState.form[txtKey]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<InputGroup.Addon align="inline-start">
|
||||
<DropdownMenu.Root
|
||||
@ -322,9 +257,7 @@
|
||||
{...props}
|
||||
variant="ghost" disabled={disabledSign}
|
||||
>
|
||||
{formState.selectOptions?.[key]?.find(
|
||||
opt => opt.value === formState.form[key]
|
||||
)?.label || 'Choose'}
|
||||
{formState.selectOptions?.[key]?.find(opt => opt.value === formState.form[key])?.label || 'Choose'}
|
||||
<ChevronDownIcon />
|
||||
</InputGroup.Button>
|
||||
{/snippet}
|
||||
@ -335,12 +268,27 @@
|
||||
Loading...
|
||||
</DropdownMenu.Item>
|
||||
{:else}
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
formState.form[key] = "";
|
||||
dropdownOpen[key] = false;
|
||||
formState.validateField("LowSign");
|
||||
formState.validateField("HighSign");
|
||||
}}
|
||||
>
|
||||
- None -
|
||||
</DropdownMenu.Item>
|
||||
{#each formState.selectOptions?.[key] || [] as option}
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
formState.form[key] = option.value;
|
||||
dropdownOpen[key] = false;
|
||||
}}
|
||||
onSelect={() => {
|
||||
formState.form[key] = option.value;
|
||||
formState.validateField("LowSign");
|
||||
formState.validateField("HighSign");
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</DropdownMenu.Item>
|
||||
@ -353,19 +301,43 @@
|
||||
{:else if type === "agejoin"}
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input type="number" bind:value={joinFields[key].YY}/>
|
||||
<InputGroup.Input type="number" bind:value={joinFields[key].YY}
|
||||
oninput={() => {
|
||||
if (validateOn?.includes("input")) {
|
||||
// formState.validateField(key, formState.form[key], false);
|
||||
formState.validateField("AgeStart");
|
||||
formState.validateField("AgeEnd");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<InputGroup.Text>Year</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input type="number" bind:value={joinFields[key].MM}/>
|
||||
<InputGroup.Input type="number" bind:value={joinFields[key].MM}
|
||||
oninput={() => {
|
||||
if (validateOn?.includes("input")) {
|
||||
// formState.validateField(key, formState.form[key], false);
|
||||
formState.validateField("AgeStart");
|
||||
formState.validateField("AgeEnd");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<InputGroup.Text>Month</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input type="number" bind:value={joinFields[key].DD}/>
|
||||
<InputGroup.Input type="number" bind:value={joinFields[key].DD}
|
||||
oninput={() => {
|
||||
if (validateOn?.includes("input")) {
|
||||
// formState.validateField(key, formState.form[key], false);
|
||||
formState.validateField("AgeStart");
|
||||
formState.validateField("AgeEnd");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<InputGroup.Text>Day</InputGroup.Text>
|
||||
</InputGroup.Addon>
|
||||
@ -388,9 +360,9 @@
|
||||
{formState.errors[key]}
|
||||
</span>
|
||||
{/if} -->
|
||||
{#if formState.errors[key]}
|
||||
{#if formState.errors[key] || formState.errors[txtKey]}
|
||||
<span class="text-destructive text-sm leading-none">
|
||||
{formState.errors[key]}
|
||||
{formState.errors[key] ?? formState.errors[txtKey]}
|
||||
</span>
|
||||
<!-- {:else if dynamicRequiredFields.includes(key)}
|
||||
<span class="text-destructive text-sm leading-none">
|
||||
|
||||
@ -31,4 +31,32 @@ export function daysToAge(totalDays) {
|
||||
const DD = remainingAfterYear % 30;
|
||||
|
||||
return { YY, MM, DD };
|
||||
}
|
||||
|
||||
// export function toDays(ageString) {
|
||||
// if (!ageString || typeof ageString !== 'string') return null;
|
||||
|
||||
// const match = ageString.match(/(\d+)\s*Y?\s*(\d+)\s*M?\s*(\d+)\s*D?/i);
|
||||
|
||||
// if (!match) return null;
|
||||
|
||||
// const YY = parseInt(match[1]) || 0;
|
||||
// const MM = parseInt(match[2]) || 0;
|
||||
// const DD = parseInt(match[3]) || 0;
|
||||
|
||||
// return YY * 365 + MM * 30 + DD;
|
||||
// }
|
||||
|
||||
export function toDays(ageString) {
|
||||
if (!ageString || typeof ageString !== "string") return null;
|
||||
|
||||
const match = ageString.match(/(?:(\d+)\s*Y)?\s*(?:(\d+)\s*M)?\s*(?:(\d+)\s*D)?/i);
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
const YY = parseInt(match[1] || 0);
|
||||
const MM = parseInt(match[2] || 0);
|
||||
const DD = parseInt(match[3] || 0);
|
||||
|
||||
return YY * 365 + MM * 30 + DD;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user