Merge branch 'dev' into refactor/mv-flow-to-content

This commit is contained in:
Khafid Prayoga
2025-09-08 13:41:33 +07:00
9 changed files with 171 additions and 125 deletions
+120 -58
View File
@@ -1,70 +1,132 @@
<script setup lang="ts">
import Block from '~/components/pub/custom-ui/form/block.vue'
import FieldGroup from '~/components/pub/custom-ui/form/field-group.vue'
import Field from '~/components/pub/custom-ui/form/field.vue'
// types
import type z from 'zod'
import type { MaterialFormData } from '~/schemas/material'
// helpers
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
// components
import Label from '~/components/pub/custom-ui/form/label.vue'
const props = defineProps<{ modelValue: any; errors: any }>()
const emit = defineEmits(['update:modelValue', 'event'])
interface Props {
isLoading: boolean
schema: z.ZodSchema<any>
uoms: any[]
items: any[]
}
const data = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
},
const props = defineProps<Props>()
const emit = defineEmits<{
back: []
submit: [data: any]
}>()
const { handleSubmit, defineField, errors } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
code: '',
name: '',
uom_code: '',
item_id: '',
stock: 0,
} as Partial<MaterialFormData>,
})
const items = [
{ value: 'item1', label: 'Item 1' },
{ value: 'item2', label: 'Item 2' },
]
const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name')
const [uom, uomAttrs] = defineField('uom_code')
const [item, itemAttrs] = defineField('item_id')
const [stock, stockAttrs] = defineField('stock')
const onSubmit = handleSubmit(async (values) => {
try {
emit('submit', values)
} catch (error) {
console.error('Submission failed:', error)
}
})
</script>
<template>
<form id="entry-form">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<Block>
<FieldGroup :column="1">
<Label>Kode</Label>
<Field>
<Input v-model="data.code" />
</Field>
</FieldGroup>
<FieldGroup v-if="!!props.errors.code">
<Label></Label>
<span class="text-red-400 text-sm">{{ props.errors.code }}</span>
</FieldGroup>
<FieldGroup :column="1">
<Label>Nama</Label>
<Field>
<Input v-model="data.name" />
</Field>
</FieldGroup>
<FieldGroup v-if="!!props.errors.name">
<Label></Label>
<span class="text-red-400 text-sm">{{ props.errors.name }}</span>
</FieldGroup>
<FieldGroup :column="1">
<Label>Item</Label>
<Field>
<Select v-model="data.type" :items="items" placeholder="Pilih item" />
</Field>
</FieldGroup>
<FieldGroup :column="1">
<Label>Satuan</Label>
<Field>
<Select v-model="data.uom" :items="items" placeholder="Pilih item" />
</Field>
</FieldGroup>
<FieldGroup :column="1">
<Label>Stok</Label>
<Field>
<Input v-model="data.stock" type="number" />
</Field>
</FieldGroup>
</Block>
</div>
<form class="grid gap-2" @submit="onSubmit">
<div class="grid gap-2">
<Label for="code">Kode</Label>
<Input
id="code"
v-model="code"
v-bind="codeAttrs"
:disabled="isLoading"
:class="{ 'border-red-500': errors.code }"
/>
<span v-if="errors.code" class="text-sm text-red-500">
{{ errors.code }}
</span>
</div>
<div class="grid gap-2">
<Label for="name">Nama</Label>
<Input
id="name"
v-model="name"
v-bind="nameAttrs"
:disabled="isLoading"
:class="{ 'border-red-500': errors.name }"
/>
<span v-if="errors.name" class="text-sm text-red-500">
{{ errors.name }}
</span>
</div>
<div class="grid gap-2">
<Label for="uom">Satuan</Label>
<Select
id="uom"
v-model="uom"
icon-name="i-lucide-chevron-down"
placeholder="Pilih satuan"
v-bind="uomAttrs"
:items="uoms"
:disabled="isLoading"
:class="{ 'border-red-500': errors.uom_code }"
/>
<span v-if="errors.uom_code" class="text-sm text-red-500">
{{ errors.uom_code }}
</span>
</div>
<div class="grid gap-2">
<Label for="item">Item</Label>
<Select
id="item"
v-model="item"
icon-name="i-lucide-chevron-down"
placeholder="Pilih item"
v-bind="itemAttrs"
:items="items"
:disabled="isLoading"
:class="{ 'border-red-500': errors.item_id }"
/>
<span v-if="errors.item_id" class="text-sm text-red-500">
{{ errors.item_id }}
</span>
</div>
<div class="grid gap-2">
<Label for="stock">Stok</Label>
<Input
id="stock"
v-model="stock"
type="number"
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="my-2 flex justify-end gap-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>
</form>
</template>
+3 -4
View File
@@ -3,7 +3,6 @@ import type {
KeyLabel,
RecComponent,
RecStrFuncComponent,
RecStrFuncUnknown,
Th,
} from '~/components/pub/custom-ui/data/types'
import { defineAsyncComponent } from 'vue'
@@ -12,11 +11,11 @@ type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
export const cols: Col[] = [{ width: 100 }, { width: 250 }, { width: 100 }, { width: 100 }, { width: 50 }]
export const cols: Col[] = [{ width: 100 }, { width: 250 }, { width: 100 }, { width: 100 }, { width: 100 }, { width: 50 }]
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Item' }, { label: 'Satuan' }]]
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Stok' }, { label: 'Item' }, { label: 'Satuan' }]]
export const keys = ['code', 'name', 'item_id', 'uom_code', 'action']
export const keys = ['code', 'name', 'stock', 'item_id', 'uom_code', 'action']
export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' },
+23 -53
View File
@@ -1,64 +1,34 @@
<script setup lang="ts">
import { z, ZodError } from 'zod'
import Action from '~/components/pub/custom-ui/nav-footer/ba-dr-su.vue'
// types
import type { MaterialFormData } from '~/schemas/material'
import { MaterialSchema } from '~/schemas/material'
const errors = ref({})
const data = ref({
code: '',
name: '',
type: '',
stock: 0,
})
const isLoading = ref(false)
const uoms = [
{ value: 'uom-1', label: 'Satuan 1' },
{ value: 'uom-2', label: 'Satuan 2' },
{ value: 'uom-3', label: 'Satuan 3' },
]
const items = [
{ value: 'item-1', label: 'Item 1' },
{ value: 'item-2', label: 'Item 2' },
{ value: 'item-3', label: 'Item 3' },
]
const schema = z.object({
code: z.string().min(1, 'Code must be at least 1 characters long'),
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: 'Stok harus berupa angka' })),
})
function onBack() {
navigateTo('/tools-equipment-src/equipment')
}
function onClick(type: string) {
if (type === 'cancel') {
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)
}
async function onSubmit(data: MaterialFormData) {
console.log(data)
}
</script>
<template>
<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" />
<span class="font-semibold">Tambah</span> Alat Kesehatan
</div>
<AppMaterialEntryForm v-model="data" :errors="errors" />
<div class="my-2 flex justify-end py-2">
<Action @click="onClick" />
<Icon name="i-lucide-panel-bottom" class="me-2" />
<span class="font-semibold">Tambah</span> Perlengkapan (BMHP)
</div>
<AppMaterialEntryForm :is-loading="isLoading" :schema="MaterialSchema" :uoms="uoms" :items="items" @back="onBack"
@submit="onSubmit" />
</template>
+7 -7
View File
@@ -26,21 +26,21 @@ const recAction = ref<string>('')
const recItem = ref<any>(null)
const headerPrep: HeaderPrep = {
title: 'BMHP',
icon: 'i-lucide-paint-bucket',
title: 'Perlengkapan (BMHP)',
icon: 'i-lucide-panel-bottom',
addNav: {
label: 'Tambah',
onClick: () => navigateTo('/tools-equipment-src/material/add'),
onClick: () => navigateTo('/tools-equipment-src/equipment/add'),
},
}
async function getMaterialList() {
isLoading.dataListLoading = true
const resp = await xfetch('/api/v1/material')
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
}
// const resp = await xfetch('/api/v1/material')
// if (resp.success) {
// data.value = (resp.body as Record<string, any>).data
// }
isLoading.dataListLoading = false
}
+2 -1
View File
@@ -18,6 +18,7 @@ interface Item {
const props = defineProps<
SelectRootProps & {
items: Item[]
iconName?: string
placeholder?: string
label?: string
separator?: boolean
@@ -30,7 +31,7 @@ const forwarded = useForwardPropsEmits(props, emits)
<template>
<SelectRoot v-bind="forwarded">
<SelectTrigger class="">
<SelectTrigger :icon-name="iconName" class="flex justify-between items-center">
<SelectValue :placeholder="placeholder" />
</SelectTrigger>
@@ -5,15 +5,15 @@ import { SelectIcon, SelectTrigger, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'], iconName?: string }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
const iconName = computed(() => props.iconName || 'i-radix-icons-caret-sort')
</script>
<template>
+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' }).min(1, 'Kode unit harus diisi'),
item_id: z.string({ required_error: 'Tipe harus diisi' }).min(1, 'Tipe harus diisi'),
stock: z.preprocess((val) => Number(val), z.number({ invalid_type_error: 'Stok harus berupa angka' }).min(1, 'Stok harus lebih besar dari 0')),
})
type formData = z.infer<typeof schema>
export { schema as MaterialSchema }
export type { formData as MaterialFormData }