refactor(glob:form): update form handling and type definitions

- Migrate from Form component to vee-validate useForm
- Update combobox component to support number values
- Modify base model ID type for mock data
- Improve type safety in treatment report schema
- Add proper form submission handling
This commit is contained in:
Khafid Prayoga
2025-11-25 20:46:04 +07:00
parent 3fbcdf9e2a
commit 6a29fdfd50
7 changed files with 86 additions and 40 deletions
@@ -27,7 +27,7 @@ const { class: containerClass, labelClass, colSpan = 1, doctors = [] } = props
const opts = computed(() => { const opts = computed(() => {
return doctors.map((doc) => ({ return doctors.map((doc) => ({
value: doc.id.toString(), value: doc.id,
label: parseName(doc.employee.person as Person), label: parseName(doc.employee.person as Person),
})) }))
}) })
@@ -1,7 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'
import { TreatmentReportSchema } from '~/schemas/treatment-report.schema' // schema
import { type TreatmentReportFormData, TreatmentReportSchema } from '~/schemas/treatment-report.schema'
// type // type
import type { Doctor } from '~/models/doctor' import type { Doctor } from '~/models/doctor'
@@ -16,18 +19,50 @@ import { SelectDoctor } from '~/components/app/doctor/fields'
// Helpers // Helpers
// #region Props & Emits // #region Props & Emits
interface FormData extends TreatmentReportFormData {
hiddenNyc: number
}
interface Props { interface Props {
isLoading: boolean isLoading: boolean
mode?: 'create' | 'update' | 'view' mode?: 'create' | 'update' | 'view'
initialValues?: any initialValues?: Partial<FormData>
// form related // form related
doctors: Doctor[] doctors: Doctor[]
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const { mode = 'create' } = props const emit = defineEmits<{
(e: 'submit', payload: FormData): void
}>()
const { isLoading, mode = 'create' } = props
const isReadonly = computed(() => {
if (isLoading) {
return true
}
if (mode === 'view') {
return true
}
return false
})
const formSchema = toTypedSchema(TreatmentReportSchema)
const { handleSubmit, values, resetForm, setFieldValue, setValues, validate } = useForm<FormData>({
name: 'treatmentReportForm',
validationSchema: formSchema,
initialValues: props.initialValues ? props.initialValues : {},
validateOnMount: false,
})
defineExpose({
validate,
resetForm,
setValues,
values,
})
// #endregion // #endregion
// #region State & Computed // #region State & Computed
@@ -40,28 +75,12 @@ const { mode = 'create' } = props
// #endregion region // #endregion region
// #region Utilities & event handlers // #region Utilities & event handlers
const onSubmit = handleSubmit((formValues: FormData) => emit('submit', formValues))
// #endregion // #endregion
const formSchema = toTypedSchema(TreatmentReportSchema)
const formRef = ref()
defineExpose({
validate: () => formRef.value?.validate(),
resetForm: () => formRef.value?.resetForm(),
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
values: computed(() => formRef.value?.values),
})
</script> </script>
<template> <template>
<Form <form @submit="onSubmit">
ref="formRef"
as=""
keep-values
:validation-schema="formSchema"
:validate-on-mount="false"
:initial-values="initialValues ? initialValues : {}"
validation-mode="onSubmit"
>
<Fragment <Fragment
v-slot="{ section }" v-slot="{ section }"
title="Tim Pelaksana Tindakan" title="Tim Pelaksana Tindakan"
@@ -73,13 +92,14 @@ defineExpose({
:cell-flex="false" :cell-flex="false"
> >
<SelectDoctor <SelectDoctor
:doctors="doctors" fieldName="operatorTeam.dpjpId"
fieldName="dpjp"
label="Dokter Pemeriksa" label="Dokter Pemeriksa"
placeholder="Pilih dokter" placeholder="Pilih dokter"
:doctors="doctors"
:is-disabled="isReadonly"
/> />
<InputBase <InputBase
field-name="operatorId" field-name="operatorTeam.operatorId"
label="Operator" label="Operator"
placeholder="Masukkan operator" placeholder="Masukkan operator"
/> />
@@ -160,5 +180,14 @@ defineExpose({
/> />
</DE.Block> </DE.Block>
</Fragment> </Fragment>
</Form> <div class="mt-4 flex justify-end">
<button
type="submit"
class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
:disabled="isLoading"
>
{{ isLoading ? 'Menyimpan...' : 'Simpan' }}
</button>
</div>
</form>
</template> </template>
@@ -4,6 +4,7 @@ import { genDoctor, type Doctor } from '~/models/doctor'
// components // components
import AppTreatmentReportEntry from '~/components/app/treatment-report/entry-form.vue' import AppTreatmentReportEntry from '~/components/app/treatment-report/entry-form.vue'
import type { TreatmentReportFormData } from '~/schemas/treatment-report.schema'
const doctors = ref<Doctor[]>([]) const doctors = ref<Doctor[]>([])
@@ -17,5 +18,12 @@ const doctors = ref<Doctor[]>([])
<AppTreatmentReportEntry <AppTreatmentReportEntry
:isLoading="false" :isLoading="false"
:doctors="doctors" :doctors="doctors"
:initialValues="
{
operatorTeam: {
// dpjpId: -1,
},
} as TreatmentReportFormData
"
/> />
</template> </template>
@@ -5,7 +5,7 @@ import { type Item } from './index'
const props = defineProps<{ const props = defineProps<{
id?: string id?: string
modelValue?: string modelValue?: string | number
items: Item[] items: Item[]
placeholder?: string placeholder?: string
searchPlaceholder?: string searchPlaceholder?: string
@@ -15,8 +15,8 @@ const props = defineProps<{
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
'update:modelValue': [value: string] 'update:modelValue': [value: string | number]
'update:searchText': [value: string] 'update:searchText': [value: string | number]
}>() }>()
const open = ref(false) const open = ref(false)
+4 -4
View File
@@ -1,5 +1,5 @@
export interface Item { export interface Item {
value: string value: string | number
label: string label: string
code?: string code?: string
priority?: number priority?: number
@@ -7,12 +7,12 @@ export interface Item {
export function recStrToItem(input: Record<string, string>): Item[] { export function recStrToItem(input: Record<string, string>): Item[] {
const items: Item[] = [] const items: Item[] = []
let idx = 0; let idx = 0
for (const key in input) { for (const key in input) {
if (input.hasOwnProperty(key)) { if (input.hasOwnProperty(key)) {
items.push({ items.push({
value: key || ('unknown-' + idx), value: key || 'unknown-' + idx,
label: input[key] || ('unknown-' + idx), label: input[key] || 'unknown-' + idx,
}) })
} }
idx++ idx++
+3 -2
View File
@@ -1,4 +1,3 @@
export interface Base { export interface Base {
id: number id: number
createdAt: string | null createdAt: string | null
@@ -20,7 +19,9 @@ export interface TreeItem {
export function genBase(): Base { export function genBase(): Base {
return { return {
id: 0, // -1 buat mock data
// backend harusnya non-negative/ > 0 (untuk auto increment constraint) jadi harusnya aman ya
id: -1,
createdAt: '', createdAt: '',
updatedAt: '', updatedAt: '',
} }
+12 -4
View File
@@ -4,10 +4,18 @@ const isoDateTime = z.string().min(1, 'Tanggal / waktu wajib diisi')
const positiveInt = z.number().int().nonnegative() const positiveInt = z.number().int().nonnegative()
const OperatorTeamSchema = z.object({ const OperatorTeamSchema = z.object({
dpjpId: z.number().int(), dpjpId: z.coerce
operatorId: z.number().int(), .number({
assistantOperatorId: z.number().int().optional().nullable(), invalid_type_error: 'Dokter Pemeriksa wajib diisi',
instrumentNurseId: z.number().int().optional().nullable(), })
.int(),
operatorId: z.coerce
.number({
invalid_type_error: 'Operator wajib diisi',
})
.int(),
assistantOperatorId: z.coerce.number().int().optional().nullable(),
instrumentNurseId: z.coerce.number().int().optional().nullable(),
surgeryDate: isoDateTime, surgeryDate: isoDateTime,
actionDiagnosis: z.string().min(1), actionDiagnosis: z.string().min(1),