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"> <script setup lang="ts">
import Block from '~/components/pub/custom-ui/form/block.vue' // types
import FieldGroup from '~/components/pub/custom-ui/form/field-group.vue' import type z from 'zod'
import Field from '~/components/pub/custom-ui/form/field.vue' 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' 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>
uoms: 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 } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
code: '',
name: '',
uom_code: '',
item_id: '',
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) => {
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> v-model="uom"
<Field> icon-name="i-lucide-chevron-down"
<Select v-model="data.uom" :items="items" placeholder="Pilih item" /> placeholder="Pilih satuan"
</Field> v-bind="uomAttrs"
</FieldGroup> :items="uoms"
<FieldGroup :column="1"> :disabled="isLoading"
<Label>Stok</Label> :class="{ 'border-red-500': errors.uom_code }"
<Field> />
<Input v-model="data.stock" type="number" /> <span v-if="errors.uom_code" class="text-sm text-red-500">
</Field> {{ errors.uom_code }}
</FieldGroup> </span>
</Block> </div>
</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> </div>
</form> </form>
</template> </template>
+3 -4
View File
@@ -3,7 +3,6 @@ import type {
KeyLabel, KeyLabel,
RecComponent, RecComponent,
RecStrFuncComponent, RecStrFuncComponent,
RecStrFuncUnknown,
Th, Th,
} from '~/components/pub/custom-ui/data/types' } from '~/components/pub/custom-ui/data/types'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
@@ -12,11 +11,11 @@ type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue')) 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[] = [ export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' }, { key: 'code', label: 'Kode' },
+23 -53
View File
@@ -1,64 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import { z, ZodError } from 'zod' // types
import Action from '~/components/pub/custom-ui/nav-footer/ba-dr-su.vue' import type { MaterialFormData } from '~/schemas/material'
import { MaterialSchema } from '~/schemas/material'
const errors = ref({}) const isLoading = ref(false)
const data = ref({ const uoms = [
code: '', { value: 'uom-1', label: 'Satuan 1' },
name: '', { value: 'uom-2', label: 'Satuan 2' },
type: '', { value: 'uom-3', label: 'Satuan 3' },
stock: 0, ]
}) const items = [
{ value: 'item-1', label: 'Item 1' },
{ value: 'item-2', label: 'Item 2' },
{ value: 'item-3', label: 'Item 3' },
]
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: 'Stok harus berupa angka' })),
})
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> Alat Kesehatan <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" :uoms="uoms" :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
} }
+2 -1
View File
@@ -18,6 +18,7 @@ interface Item {
const props = defineProps< const props = defineProps<
SelectRootProps & { SelectRootProps & {
items: Item[] items: Item[]
iconName?: string
placeholder?: string placeholder?: string
label?: string label?: string
separator?: boolean separator?: boolean
@@ -30,7 +31,7 @@ const forwarded = useForwardPropsEmits(props, emits)
<template> <template>
<SelectRoot v-bind="forwarded"> <SelectRoot v-bind="forwarded">
<SelectTrigger class=""> <SelectTrigger :icon-name="iconName" class="flex justify-between items-center">
<SelectValue :placeholder="placeholder" /> <SelectValue :placeholder="placeholder" />
</SelectTrigger> </SelectTrigger>
@@ -5,15 +5,15 @@ import { SelectIcon, SelectTrigger, useForwardProps } from 'radix-vue'
import { computed } from 'vue' import { computed } from 'vue'
import { cn } from '~/lib/utils' 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 delegatedProps = computed(() => {
const { class: _, ...delegated } = props const { class: _, ...delegated } = props
return delegated return delegated
}) })
const forwardedProps = useForwardProps(delegatedProps) const forwardedProps = useForwardProps(delegatedProps)
const iconName = computed(() => props.iconName || 'i-radix-icons-caret-sort')
</script> </script>
<template> <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 }