mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-23 09:39:27 +07:00
continue dict test; add feature calculation val
This commit is contained in:
parent
10b6e5bda3
commit
ea43e44809
@ -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, modeOpt, saveEndpoint, editEndpoint}) {
|
||||
export function useForm({schema, initialForm, defaultErrors = {}, mode = 'create', modeOpt = 'default', saveEndpoint = null, editEndpoint = null}) {
|
||||
const state = useFormState(initialForm);
|
||||
const val = useFormValidation(schema, state.form, defaultErrors, mode);
|
||||
const options = useFormOptions(modeOpt);
|
||||
|
||||
@ -3,7 +3,30 @@ import EraserIcon from "@lucide/svelte/icons/eraser";
|
||||
import { z } from "zod";
|
||||
import { cleanEmptyStrings } from "$lib/utils/cleanEmptyStrings";
|
||||
|
||||
export const testSchema = z.object({});
|
||||
export const testSchema = z.object({
|
||||
TestSiteCode: z.string().min(1, "Required"),
|
||||
TestSiteName: z.string().min(1, "Required"),
|
||||
});
|
||||
|
||||
export const testCalSchema = z.object({
|
||||
FormulaInput: z.array(z.string()),
|
||||
FormulaCode: z.string()
|
||||
}).refine(
|
||||
(data) => {
|
||||
if (data.FormulaInput.length === 0) return true;
|
||||
function hasExactKeyword(input, keyword) {
|
||||
const regex = new RegExp(`\\b${keyword}\\b`, 'i');
|
||||
return regex.test(input);
|
||||
}
|
||||
return data.FormulaInput.every((v) => hasExactKeyword(data.FormulaCode, v));
|
||||
},
|
||||
{
|
||||
message: 'Formula must contain all selected input parameters',
|
||||
path: ['FormulaCode']
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
export const testInitialForm = {
|
||||
TestSiteID: "",
|
||||
@ -62,7 +85,10 @@ export const refNumInitialForm = {
|
||||
Notes: "",
|
||||
}
|
||||
|
||||
export const testDefaultErrors = {};
|
||||
export const testDefaultErrors = {
|
||||
TestSiteCode: "Required",
|
||||
TestSiteName: "Required",
|
||||
};
|
||||
|
||||
export const testCalDefaultErrors = {};
|
||||
|
||||
@ -106,7 +132,7 @@ export const testFormFields = [
|
||||
{
|
||||
key: "TestType",
|
||||
label: "Test Type",
|
||||
required: true,
|
||||
required: false,
|
||||
type: "select",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/test_type`,
|
||||
}
|
||||
@ -363,6 +389,7 @@ export const testCalFormFields = [
|
||||
label: "Formula",
|
||||
required: false,
|
||||
type: "textarea",
|
||||
validateOn: ["input"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -518,4 +545,23 @@ export function getTestFormActions(handlers) {
|
||||
onClick: handlers.clearForm,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function buildTestPayload({
|
||||
mainForm,
|
||||
calForm,
|
||||
testType
|
||||
}) {
|
||||
let payload = {
|
||||
...mainForm
|
||||
};
|
||||
|
||||
if (testType === 'CALC') {
|
||||
payload = {
|
||||
...payload,
|
||||
...calForm
|
||||
};
|
||||
}
|
||||
|
||||
return cleanEmptyStrings(payload);
|
||||
}
|
||||
@ -6,10 +6,11 @@
|
||||
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 { testCalInitialForm, testCalDefaultErrors, testCalFormFields, refNumInitialForm, refNumFormFields } from "$lib/components/dictionary/test/config/test-form-config";
|
||||
import { buildTestPayload, testCalSchema, testCalInitialForm, testCalDefaultErrors, testCalFormFields, refNumInitialForm, refNumFormFields } from "$lib/components/dictionary/test/config/test-form-config";
|
||||
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||
import RefNum from "./reference/ref-num.svelte";
|
||||
import RefTxt from "./reference/ref-txt.svelte";
|
||||
import RefNum from "./tabs/ref-num.svelte";
|
||||
import RefTxt from "./tabs/ref-txt.svelte";
|
||||
import Calculation from "./tabs/calculation.svelte";
|
||||
|
||||
let props = $props();
|
||||
|
||||
@ -20,11 +21,6 @@
|
||||
const calFormState = useForm({
|
||||
schema: null,
|
||||
initialForm: testCalInitialForm,
|
||||
defaultErrors: {},
|
||||
mode: 'create',
|
||||
modeOpt: 'default',
|
||||
saveEndpoint: null,
|
||||
editEndpoint: null,
|
||||
});
|
||||
|
||||
const refNumState = useForm({
|
||||
@ -50,10 +46,19 @@
|
||||
let showConfirm = $state(false);
|
||||
|
||||
async function handleSave() {
|
||||
const result = await formState.save(masterDetail.mode);
|
||||
const mainForm = masterDetail.formState.form;
|
||||
const calForm = calFormState.form;
|
||||
|
||||
toast('Test Created!');
|
||||
masterDetail?.exitForm(true);
|
||||
const payload = buildTestPayload({
|
||||
mainForm,
|
||||
calForm,
|
||||
testType: mainForm.TestType
|
||||
});
|
||||
console.log(payload);
|
||||
// const result = await formState.save(masterDetail.mode);
|
||||
|
||||
// toast('Test Created!');
|
||||
// masterDetail?.exitForm(true);
|
||||
}
|
||||
|
||||
const primaryAction = $derived({
|
||||
@ -134,6 +139,7 @@
|
||||
activeTab = availableTabs[0];
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<FormPageContainer title="Create Test" {primaryAction} {secondaryActions} {actions}>
|
||||
@ -165,10 +171,7 @@
|
||||
/>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="calculation">
|
||||
<DictionaryFormRenderer
|
||||
formState={calFormState}
|
||||
formFields={testCalFormFields}
|
||||
/>
|
||||
<Calculation {calFormState}/>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="group">
|
||||
group
|
||||
|
||||
158
src/lib/components/dictionary/test/page/tabs/calculation.svelte
Normal file
158
src/lib/components/dictionary/test/page/tabs/calculation.svelte
Normal file
@ -0,0 +1,158 @@
|
||||
<script>
|
||||
import * as Select from '$lib/components/ui/select/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import { testCalSchema } from '$lib/components/dictionary/test/config/test-form-config';
|
||||
import { API } from '$lib/config/api';
|
||||
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||
import { Label } from "$lib/components/ui/label/index.js";
|
||||
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
|
||||
112
src/lib/components/dictionary/test/page/test.js
Normal file
112
src/lib/components/dictionary/test/page/test.js
Normal file
@ -0,0 +1,112 @@
|
||||
<script>
|
||||
import * as Select from '$lib/components/ui/select/index.js';
|
||||
import { fruits } from '$lib/components/multiselect/multiselect-form-config';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { z } from 'zod';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
|
||||
let value = $state([]);
|
||||
let inputValue = $state('');
|
||||
|
||||
function hasExactKeyword(input, keyword) {
|
||||
const regex = new RegExp(`\\b${keyword}\\b`, 'i');
|
||||
return regex.test(input);
|
||||
}
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
selected: z.array(z.string()),
|
||||
input: z.string()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
return data.selected.every((v) => hasExactKeyword(data.input, v));
|
||||
},
|
||||
{
|
||||
message: 'Input must contain all selected values',
|
||||
path: ['input']
|
||||
}
|
||||
);
|
||||
|
||||
let errors = $state({});
|
||||
|
||||
$effect(() => {
|
||||
const result = schema.safeParse({ selected: value, input: inputValue });
|
||||
if (!result.success) {
|
||||
const fieldErrors = {};
|
||||
for (const issue of result.error.issues) {
|
||||
fieldErrors[issue.path[0]] = issue.message;
|
||||
}
|
||||
errors = fieldErrors;
|
||||
} else {
|
||||
errors = {};
|
||||
}
|
||||
});
|
||||
|
||||
const selectedLabel = $derived(
|
||||
value.length === 0
|
||||
? 'Select a fruit'
|
||||
: value.map((v) => fruits.find((f) => f.value === v)?.value).join(', ')
|
||||
);
|
||||
|
||||
const hasErrors = $derived(Object.keys(errors).length > 0);
|
||||
|
||||
const inputStatus = $derived(
|
||||
value
|
||||
.filter((v) => v)
|
||||
.map((v) => ({
|
||||
value: v,
|
||||
label: fruits.find((f) => f.value === v)?.label,
|
||||
done: hasExactKeyword(inputValue, v)
|
||||
}))
|
||||
);
|
||||
|
||||
function unselectAll() {
|
||||
value = [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Select.Root type="multiple" name="favoriteFruit" bind:value>
|
||||
<Select.Trigger class="w-full">
|
||||
{selectedLabel}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Fruits</Select.Label>
|
||||
{#each fruits as fruit (fruit.value)}
|
||||
<Select.Item value={fruit.value} label={fruit.label}>
|
||||
{fruit.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Group>
|
||||
{#if value.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}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
|
||||
<Input bind:value={inputValue} />
|
||||
{#if errors.input}
|
||||
<div class="flex items-center gap-2 text-sm text-red-500">
|
||||
<span>{errors.input}:</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}
|
||||
|
||||
<button disabled={hasErrors} class="rounded bg-blue-500 px-4 py-2 text-white disabled:opacity-50">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
@ -24,17 +24,6 @@
|
||||
|
||||
let showConfirm = $state(false);
|
||||
|
||||
// function handleExit() {
|
||||
// const ok = masterDetail.exitForm();
|
||||
// if (!ok) {
|
||||
// showConfirm = true;
|
||||
// }
|
||||
// }
|
||||
|
||||
// function confirmDiscard() {
|
||||
// masterDetail.exitForm(true);
|
||||
// }
|
||||
|
||||
async function handleSave() {
|
||||
const payload = buildPayload(formState.form);
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||
import * as InputGroup from "$lib/components/ui/input-group/index.js";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||
|
||||
let {
|
||||
formState,
|
||||
@ -61,6 +62,11 @@
|
||||
formState.form[field.key] = field.defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// function hasExactKeyword(input, keyword) {
|
||||
// const regex = new RegExp(`\\b${keyword}\\b`, 'i');
|
||||
// return regex.test(input);
|
||||
// }
|
||||
</script>
|
||||
|
||||
{#snippet Fieldset({ key, label, required, type, optionsEndpoint, options, validateOn, dependsOn, endpointParamKey, valueKey, labelKey, txtKey })}
|
||||
@ -120,7 +126,7 @@
|
||||
}}
|
||||
/>
|
||||
{:else if type === "textarea"}
|
||||
<textarea
|
||||
<Textarea
|
||||
class="flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
oninput={() => {
|
||||
if (validateOn?.includes("input")) {
|
||||
@ -133,7 +139,7 @@
|
||||
}
|
||||
}}
|
||||
bind:value={formState.form[key]}
|
||||
></textarea>
|
||||
/>
|
||||
{:else if type === "select"}
|
||||
{@const selectedLabel = formState.selectOptions?.[key]?.find(opt => opt.value === formState.form[key])?.label || "Choose"}
|
||||
{@const filteredOptions = getFilteredOptions(key)}
|
||||
@ -232,6 +238,9 @@
|
||||
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) {
|
||||
@ -280,6 +289,14 @@
|
||||
{/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
|
||||
@ -372,12 +389,34 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="absolute top-8 min-h-[1rem] w-full">
|
||||
{#if formState.errors[key]}
|
||||
<div
|
||||
class={`absolute min-h-[1rem] w-full ${
|
||||
key === 'FormulaCode' ? 'top-20' : 'top-8'
|
||||
}`}
|
||||
>
|
||||
<!-- {#if formState.errors[key]}
|
||||
<span class="text-destructive text-sm leading-none">
|
||||
{formState.errors[key]}
|
||||
</span>
|
||||
{/if} -->
|
||||
{#if key === 'FormulaCode' && formState.errors[key]}
|
||||
<div class="flex items-center gap-2 text-sm text-destructive">
|
||||
<span>{formState.errors[key]}:</span>
|
||||
<div class="flex gap-1">
|
||||
{#each formState.form.FormulaInput || [] as item}
|
||||
{@const hasItem = hasExactKeyword(formState.form.FormulaCode, item)}
|
||||
<Badge variant={hasItem ? 'default' : 'destructive'}>
|
||||
{item}
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else if formState.errors[key]}
|
||||
<span class="text-destructive text-sm leading-none">
|
||||
{formState.errors[key]}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import ViewPage from "$lib/components/dictionary/test/page/view-page.svelte";
|
||||
import CreatePage from "$lib/components/dictionary/test/page/create-page.svelte";
|
||||
import EditPage from "$lib/components/dictionary/test/page/edit-page.svelte";
|
||||
import { testSchema, testInitialForm, testDefaultErrors, testFormFields, getTestFormActions } from "$lib/components/dictionary/test/config/test-form-config";
|
||||
import { testSchema, testInitialForm, testDefaultErrors, testFormFields, getTestFormActions, testCalDefaultErrors } from "$lib/components/dictionary/test/config/test-form-config";
|
||||
|
||||
const masterDetail = useMasterDetail({
|
||||
onSelect: async (row) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user