refactor(material): add zod validation for stock field as number and display error message after submit in

This commit is contained in:
riefive
2025-09-04 15:55:32 +07:00
parent 209f78df0a
commit 21d087e496
4 changed files with 155 additions and 121 deletions
+118 -62
View File
@@ -1,74 +1,130 @@
<script setup lang="ts"> <script setup lang="ts">
import Block from '~/components/pub/custom-ui/form/block.vue' // helpers
import FieldGroup from '~/components/pub/custom-ui/form/field-group.vue' import { toTypedSchema } from '@vee-validate/zod'
import Field from '~/components/pub/custom-ui/form/field.vue' import { useForm } from 'vee-validate'
// types
import type z from 'zod'
import type { MaterialFormData } from '~/schemas/material'
// components
import Label from '~/components/pub/custom-ui/form/label.vue' import Label from '~/components/pub/custom-ui/form/label.vue'
const props = defineProps<{ modelValue: any; errors: any }>() interface Props {
const emit = defineEmits(['update:modelValue', 'event']) isLoading: boolean
schema: z.ZodSchema<any>
items: any[]
}
const data = computed({ const props = defineProps<Props>()
get: () => props.modelValue, const emit = defineEmits<{
set: (val) => { back: []
emit('update:modelValue', val) submit: [data: any]
}, }>()
const { handleSubmit, defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
code: '',
name: '',
stock: 0,
} as Partial<MaterialFormData>,
}) })
const items = [ const [code, codeAttrs] = defineField('code')
{ value: 'item1', label: 'Item 1' }, const [name, nameAttrs] = defineField('name')
{ value: 'item2', label: 'Item 2' }, const [uom, uomAttrs] = defineField('uom_code')
] const [item, itemAttrs] = defineField('item_id')
const [stock, stockAttrs] = defineField('stock')
const onSubmit = handleSubmit(async (values) => {
console.log(errors)
try {
emit('submit', values)
} catch (error) {
console.error('Submission failed:', error)
}
})
</script> </script>
<template> <template>
<form id="entry-form"> <form class="grid gap-2" @submit="onSubmit">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl"> <div class="grid gap-2">
<div class="flex flex-col justify-between"> <Label for="code">Kode</Label>
<Block> <Input
<FieldGroup :column="1"> id="code"
<Label>Kode</Label> v-model="code"
<Field> v-bind="codeAttrs"
<Input v-model="data.code" /> :disabled="isLoading"
</Field> :class="{ 'border-red-500': errors.code }"
</FieldGroup> />
<FieldGroup v-if="!!props.errors.code"> <span v-if="errors.code" class="text-sm text-red-500">
<Label></Label> {{ errors.code }}
<span class="text-red-400 text-sm">{{ props.errors.code }}</span> </span>
</FieldGroup> </div>
<FieldGroup :column="1"> <div class="grid gap-2">
<Label>Nama</Label> <Label for="name">Nama</Label>
<Field> <Input
<Input v-model="data.name" /> id="name"
</Field> v-model="name"
</FieldGroup> v-bind="nameAttrs"
<FieldGroup v-if="!!props.errors.name"> :disabled="isLoading"
<Label></Label> :class="{ 'border-red-500': errors.name }"
<span class="text-red-400 text-sm">{{ props.errors.name }}</span> />
</FieldGroup> <span v-if="errors.name" class="text-sm text-red-500">
<FieldGroup :column="1"> {{ errors.name }}
<Label>Item</Label> </span>
<Field> </div>
<Select v-model="data.type" :items="items" placeholder="Pilih item" /> <div class="grid gap-2">
</Field> <Label for="uom">Satuan</Label>
</FieldGroup> <Select
<FieldGroup :column="1"> id="uom"
<Label>Satuan</Label> :items="items"
<Field> placeholder="Pilih satuan"
<Select v-model="data.uom" :items="items" placeholder="Pilih item" /> v-model="uom"
</Field> v-bind="uomAttrs"
</FieldGroup> :disabled="isLoading"
<FieldGroup :column="1"> :class="{ 'border-red-500': errors.uom }"
<Label>Stok</Label> />
<Field> <span v-if="errors.uom" class="text-sm text-red-500">
<Input v-model="data.stock" type="number" /> {{ errors.uom }}
</Field> </span>
</FieldGroup> </div>
<FieldGroup v-if="!!props.errors.stock"> <div class="grid gap-2">
<Label></Label> <Label for="item">Item</Label>
<span class="text-red-400 text-sm">{{ props.errors.stock }}</span> <Select
</FieldGroup> id="item"
</Block> :items="items"
</div> placeholder="Pilih item"
v-model="item"
v-bind="itemAttrs"
:disabled="isLoading"
:class="{ 'border-red-500': errors.item }"
/>
<span v-if="errors.item" class="text-sm text-red-500">
{{ errors.uom }}
</span>
</div>
<div class="grid gap-2">
<Label for="stock">Stok</Label>
<Input
id="stock"
type="number"
v-model="stock"
v-bind="stockAttrs"
:disabled="isLoading"
:class="{ 'border-red-500': errors.stock }"
/>
<span v-if="errors.stock" class="text-sm text-red-500">
{{ errors.stock }}
</span>
</div>
<div class="flex justify-end gap-2 my-2 py-2">
<Button variant="secondary" class="w-[120px]" @click="emit('back')">
Kembali
</Button>
<Button type="submit" class="w-[120px]">
<Loader2 v-if="isLoading" class="mr-2 h-4 w-4 animate-spin" />
Simpan
</Button>
</div> </div>
</form> </form>
</template> </template>
+16 -52
View File
@@ -1,64 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
// types
import { MaterialSchema, type MaterialFormData } from '~/schemas/material'
// components
import Action from '~/components/pub/custom-ui/nav-footer/ba-dr-su.vue' import Action from '~/components/pub/custom-ui/nav-footer/ba-dr-su.vue'
import { z, ZodError } from 'zod'
const errors = ref({}) const isLoading = ref(false)
const data = ref({ const items = [
code: '', { value: 'item1', label: 'Item 1' },
name: '', { value: 'item2', label: 'Item 2' },
type: '', ]
stock: 0,
})
const schema = z.object({ function onBack() {
code: z.string().min(1, 'Code must be at least 1 characters long'), navigateTo('/tools-equipment-src/equipment')
name: z.string().min(1, 'Name must be at least 1 characters long'), }
type: z.string(),
stock: z.preprocess((val) => Number(val), z.number({ invalid_type_error: 'Stock must be a number' }).min(1)),
})
function onClick(type: string) { async function onSubmit(data: MaterialFormData) {
if (type === 'cancel') { console.log(data);
navigateTo('/tools-equipment-src/material')
} else if (type === 'draft') {
// do something
} else if (type === 'submit') {
// do something
const input = data.value
console.log(input)
const errorsParsed: any = {}
try {
const result = schema.safeParse(input)
if (!result.success) {
// You can handle the error here, e.g. show a message
const errorsCaptures = result?.error?.errors || []
const errorMessage = result.error.errors[0]?.message ?? 'Validation error occurred'
errorsCaptures.forEach((value: any) => {
const keyName = value?.path?.length > 0 ? value.path[0] : 'key'
errorsParsed[keyName as string] = value.message || ''
})
console.log(errorMessage)
}
} catch (e) {
if (e instanceof ZodError) {
const jsonError = e.flatten()
console.log(JSON.stringify(jsonError, null, 2))
}
}
setTimeout(() => {
errors.value = errorsParsed
}, 0)
}
} }
</script> </script>
<template> <template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl"> <div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<Icon name="i-lucide-paint-bucket" class="me-2" /> <Icon name="i-lucide-panel-bottom" class="me-2" />
<span class="font-semibold">Tambah</span> BMHP <span class="font-semibold">Tambah</span> Perlengkapan (BMHP)
</div>
<AppMaterialEntryForm v-model="data" :errors="errors" />
<div class="my-2 flex justify-end py-2">
<Action @click="onClick" />
</div> </div>
<AppMaterialEntryForm :is-loading="isLoading" :schema="MaterialSchema" :items="items" @back="onBack" @submit="onSubmit" />
</template> </template>
+7 -7
View File
@@ -26,21 +26,21 @@ const recAction = ref<string>('')
const recItem = ref<any>(null) const recItem = ref<any>(null)
const headerPrep: HeaderPrep = { const headerPrep: HeaderPrep = {
title: 'BMHP', title: 'Perlengkapan (BMHP)',
icon: 'i-lucide-paint-bucket', icon: 'i-lucide-panel-bottom',
addNav: { addNav: {
label: 'Tambah', label: 'Tambah',
onClick: () => navigateTo('/tools-equipment-src/material/add'), onClick: () => navigateTo('/tools-equipment-src/equipment/add'),
}, },
} }
async function getMaterialList() { async function getMaterialList() {
isLoading.dataListLoading = true isLoading.dataListLoading = true
const resp = await xfetch('/api/v1/material') // const resp = await xfetch('/api/v1/material')
if (resp.success) { // if (resp.success) {
data.value = (resp.body as Record<string, any>).data // data.value = (resp.body as Record<string, any>).data
} // }
isLoading.dataListLoading = false isLoading.dataListLoading = false
} }
+14
View File
@@ -0,0 +1,14 @@
import { z } from 'zod'
const schema = z.object({
code: z.string({ required_error: 'Kode harus diisi' }).min(1, 'Kode minimum 1 karakter'),
name: z.string({ required_error: 'Nama harus diisi' }).min(1, 'Nama minimum 1 karakter'),
uom_code: z.string({ required_error: 'Kode unit harus diisi' }),
item_id: z.string({ required_error: 'Tipe harus diisi' }),
stock: z.preprocess((val) => Number(val), z.number({ invalid_type_error: 'Stok harus berupa angka' }).min(1)),
})
type formData = z.infer<typeof schema>
export { schema as MaterialSchema }
export type { formData as MaterialFormData }