refactor(patient): rename entry component to add and clean up form error props

Remove FormErrors type imports and error props from all form components
Move content from entry.vue to add.vue with updated title styling
This commit is contained in:
Khafid Prayoga
2025-11-24 12:03:53 +07:00
parent 5c60e4e570
commit cd57dc6243
9 changed files with 320 additions and 365 deletions

No files matched your search

-23
View File
@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
import { Form } from '~/components/pub/ui/form' import { Form } from '~/components/pub/ui/form'
@@ -28,7 +27,6 @@ import {
const props = defineProps<{ const props = defineProps<{
schema: any schema: any
initialValues?: any initialValues?: any
errors?: FormErrors
}>() }>()
const formSchema = toTypedSchema(props.schema) const formSchema = toTypedSchema(props.schema)
@@ -62,7 +60,6 @@ defineExpose({
field-name="identityNumber" field-name="identityNumber"
label="No. KTP" label="No. KTP"
placeholder="Masukkan NIK" placeholder="Masukkan NIK"
:errors="errors"
numeric-only numeric-only
/> />
<InputBase <InputBase
@@ -71,68 +68,58 @@ defineExpose({
placeholder="Masukkan nomor SIM" placeholder="Masukkan nomor SIM"
numeric-only numeric-only
:max-length="20" :max-length="20"
:errors="errors"
/> />
<InputBase <InputBase
field-name="passportNumber" field-name="passportNumber"
label="No. Paspor" label="No. Paspor"
placeholder="Masukkan nomor paspor" placeholder="Masukkan nomor paspor"
:max-length="20" :max-length="20"
:errors="errors"
/> />
<InputName <InputName
field-name-alias="alias" field-name-alias="alias"
field-name-input="fullName" field-name-input="fullName"
label-for-input="Nama Lengkap" label-for-input="Nama Lengkap"
placeholder="Masukkan nama lengkap pasien" placeholder="Masukkan nama lengkap pasien"
:errors="errors"
is-required is-required
/> />
<RadioNewborn <RadioNewborn
field-name="isNewBorn" field-name="isNewBorn"
label="Pasien Bayi" label="Pasien Bayi"
placeholder="Pilih status pasien" placeholder="Pilih status pasien"
:errors="errors"
is-required is-required
/> />
<SelectGender <SelectGender
field-name="gender" field-name="gender"
label="Jenis Kelamin" label="Jenis Kelamin"
placeholder="Pilih jenis kelamin" placeholder="Pilih jenis kelamin"
:errors="errors"
is-required is-required
/> />
<SelectBirthPlace <SelectBirthPlace
field-name="birthPlace" field-name="birthPlace"
label="Tempat Lahir" label="Tempat Lahir"
placeholder="Pilih tempat lahir" placeholder="Pilih tempat lahir"
:errors="errors"
is-required is-required
/> />
<SelectDob <SelectDob
label="Tanggal Lahir" label="Tanggal Lahir"
:errors="errors"
is-required is-required
/> />
<SelectEducation <SelectEducation
field-name="education" field-name="education"
label="Pendidikan" label="Pendidikan"
placeholder="Pilih pendidikan" placeholder="Pilih pendidikan"
:errors="errors"
is-required is-required
/> />
<SelectJob <SelectJob
field-name="job" field-name="job"
label="Pekerjaan" label="Pekerjaan"
placeholder="Pilih pekerjaan" placeholder="Pilih pekerjaan"
:errors="errors"
is-required is-required
/> />
<SelectMaritalStatus <SelectMaritalStatus
field-name="maritalStatus" field-name="maritalStatus"
label="Status Perkawinan" label="Status Perkawinan"
placeholder="Pilih status Perkawinan" placeholder="Pilih status Perkawinan"
:errors="errors"
is-required is-required
/> />
<DE.Cell /> <DE.Cell />
@@ -140,46 +127,39 @@ defineExpose({
field-name="nationality" field-name="nationality"
label="Kebangsaan" label="Kebangsaan"
placeholder="Pilih kebangsaan" placeholder="Pilih kebangsaan"
:errors="errors"
is-required is-required
/> />
<SelectEthnicity <SelectEthnicity
field-name="ethnicity" field-name="ethnicity"
label="Suku" label="Suku"
placeholder="Pilih suku bangsa" placeholder="Pilih suku bangsa"
:errors="errors"
:is-disabled="values.nationality !== 'WNI'" :is-disabled="values.nationality !== 'WNI'"
/> />
<SelectLanguage <SelectLanguage
field-name="language" field-name="language"
label="Bahasa" label="Bahasa"
placeholder="Pilih preferensi bahasa" placeholder="Pilih preferensi bahasa"
:errors="errors"
is-required is-required
/> />
<SelectReligion <SelectReligion
field-name="religion" field-name="religion"
label="Agama" label="Agama"
placeholder="Pilih agama" placeholder="Pilih agama"
:errors="errors"
is-required is-required
/> />
<RadioCommunicationBarrier <RadioCommunicationBarrier
field-name="communicationBarrier" field-name="communicationBarrier"
label="Hambatan Berkomunikasi" label="Hambatan Berkomunikasi"
:errors="errors"
is-required is-required
/> />
<RadioDisability <RadioDisability
field-name="disability" field-name="disability"
label="Disabilitas" label="Disabilitas"
:errors="errors"
is-required is-required
/> />
<SelectDisability <SelectDisability
label="Jenis Disabilitas" label="Jenis Disabilitas"
field-name="disabilityType" field-name="disabilityType"
:errors="errors"
:is-disabled="values.disability !== 'YA'" :is-disabled="values.disability !== 'YA'"
:is-required="values.disability === 'YA'" :is-required="values.disability === 'YA'"
/> />
@@ -187,7 +167,6 @@ defineExpose({
field-name="note" field-name="note"
label="Kepercayaan" label="Kepercayaan"
placeholder="Contoh: tidak ingin diperiksa oleh dokter laki-laki" placeholder="Contoh: tidak ingin diperiksa oleh dokter laki-laki"
:errors="errors"
/> />
</DE.Block> </DE.Block>
@@ -202,7 +181,6 @@ defineExpose({
field-name="identityCardFile" field-name="identityCardFile"
label="Dokumen KTP" label="Dokumen KTP"
placeholder="Unggah scan dokumen KTP" placeholder="Unggah scan dokumen KTP"
:errors="errors"
:accept="['pdf', 'jpg', 'png']" :accept="['pdf', 'jpg', 'png']"
:max-size-mb="1" :max-size-mb="1"
/> />
@@ -210,7 +188,6 @@ defineExpose({
field-name="familyCardFile" field-name="familyCardFile"
label="Dokumen KK" label="Dokumen KK"
placeholder="Unggah scan dokumen KK" placeholder="Unggah scan dokumen KK"
:errors="errors"
:accept="['pdf', 'jpg', 'png']" :accept="['pdf', 'jpg', 'png']"
:max-size-mb="1" :max-size-mb="1"
/> />
@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
// components // components
@@ -17,7 +16,6 @@ const props = defineProps<{
} }
schema: any schema: any
initialValues?: any initialValues?: any
errors?: FormErrors
}>() }>()
const formSchema = toTypedSchema(props.schema) const formSchema = toTypedSchema(props.schema)
@@ -283,10 +281,7 @@ watch(
> >
Apakah alamat KTP sama dengan alamat sekarang? Apakah alamat KTP sama dengan alamat sekarang?
</DE.Label> </DE.Label>
<DE.Field <DE.Field id="isSameAddress">
id="isSameAddress"
:errors="errors"
>
<FormField <FormField
v-slot="{ componentField }" v-slot="{ componentField }"
name="isSameAddress" name="isSameAddress"
@@ -354,7 +349,6 @@ watch(
label="Alamat" label="Alamat"
:placeholder="getFieldState('address').placeholder" :placeholder="getFieldState('address').placeholder"
:is-disabled="getFieldState('address').disabled" :is-disabled="getFieldState('address').disabled"
:errors="errors"
:is-required="values.isSameAddress !== true && values.isSameAddress !== '1'" :is-required="values.isSameAddress !== true && values.isSameAddress !== '1'"
:col-span="2" :col-span="2"
/> />
@@ -362,7 +356,6 @@ watch(
<InputBase <InputBase
field-name="rt" field-name="rt"
label="RT" label="RT"
:errors="errors"
numeric-only numeric-only
:max-length="2" :max-length="2"
:placeholder="getFieldState('rt').placeholder" :placeholder="getFieldState('rt').placeholder"
@@ -373,7 +366,6 @@ watch(
label="RW" label="RW"
:placeholder="getFieldState('rw').placeholder" :placeholder="getFieldState('rw').placeholder"
:is-disabled="getFieldState('rw').disabled" :is-disabled="getFieldState('rw').disabled"
:errors="errors"
:max-length="2" :max-length="2"
numeric-only numeric-only
/> />
@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
// components // components
@@ -15,7 +14,6 @@ const props = defineProps<{
} }
schema: any schema: any
initialValues?: any initialValues?: any
errors?: FormErrors
}>() }>()
const formSchema = toTypedSchema(props.schema) const formSchema = toTypedSchema(props.schema)
@@ -207,7 +205,6 @@ watch(
field-name="address" field-name="address"
label="Alamat" label="Alamat"
placeholder="Masukkan alamat" placeholder="Masukkan alamat"
:errors="errors"
is-required is-required
:col-span="2" :col-span="2"
/> />
@@ -217,7 +214,6 @@ watch(
field-name="rt" field-name="rt"
label="RT" label="RT"
placeholder="01" placeholder="01"
:errors="errors"
numeric-only numeric-only
:max-length="2" :max-length="2"
/> />
@@ -225,7 +221,6 @@ watch(
field-name="rw" field-name="rw"
label="RW" label="RW"
placeholder="02" placeholder="02"
:errors="errors"
:max-length="2" :max-length="2"
numeric-only numeric-only
/> />
@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
// components // components
@@ -13,7 +12,6 @@ const props = defineProps<{
schema: any schema: any
contactLimit: number contactLimit: number
initialValues?: any initialValues?: any
errors?: FormErrors
}>() }>()
const { contactLimit = 5 } = props const { contactLimit = 5 } = props
@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
import { FieldArray } from 'vee-validate' import { FieldArray } from 'vee-validate'
import InputBase from '~/components/pub/my-ui/form/input-base.vue' import InputBase from '~/components/pub/my-ui/form/input-base.vue'
@@ -11,7 +10,6 @@ const props = defineProps<{
title: string title: string
schema: any schema: any
initialValues?: any initialValues?: any
errors?: FormErrors
}>() }>()
const formSchema = toTypedSchema(props.schema) const formSchema = toTypedSchema(props.schema)
@@ -82,7 +80,6 @@ defineExpose({
<SelectContactRelation <SelectContactRelation
:field-name="`contacts[${idx}].relation`" :field-name="`contacts[${idx}].relation`"
placeholder="Pilih" placeholder="Pilih"
:errors="errors"
field-group-class="mb-0" field-group-class="mb-0"
/> />
</div> </div>
@@ -98,7 +95,6 @@ defineExpose({
label="" label=""
:field-name="`contacts[${idx}].name`" :field-name="`contacts[${idx}].name`"
placeholder="Masukkan Nama" placeholder="Masukkan Nama"
:errors="errors"
/> />
</div> </div>
@@ -113,7 +109,6 @@ defineExpose({
:field-name="`contacts[${idx}].address`" :field-name="`contacts[${idx}].address`"
label="" label=""
placeholder="Masukkan Alamat" placeholder="Masukkan Alamat"
:errors="errors"
/> />
</div> </div>
@@ -130,7 +125,6 @@ defineExpose({
placeholder="081234567890" placeholder="081234567890"
:max-length="15" :max-length="15"
numeric-only numeric-only
:errors="errors"
/> />
</div> </div>
</div> </div>
@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PersonFamilyFormData as FamilyData } from '~/schemas/person-family.schema' import type { PersonFamilyFormData as FamilyData } from '~/schemas/person-family.schema'
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
import { FieldArray } from 'vee-validate' import { FieldArray } from 'vee-validate'
@@ -14,7 +13,6 @@ const props = defineProps<{
title: string title: string
schema: any schema: any
initialValues?: any initialValues?: any
errors?: FormErrors
}>() }>()
const formSchema = toTypedSchema(props.schema) const formSchema = toTypedSchema(props.schema)
@@ -118,7 +116,6 @@ defineExpose({
? 'Masukkan nama ayah pasien' ? 'Masukkan nama ayah pasien'
: 'Masukkan nama' : 'Masukkan nama'
" "
:errors="errors"
is-required is-required
/> />
</div> </div>
@@ -134,7 +131,6 @@ defineExpose({
: 'Pendidikan' : 'Pendidikan'
" "
placeholder="Pilih" placeholder="Pilih"
:errors="errors"
/> />
</div> </div>
</div> </div>
@@ -156,7 +152,6 @@ defineExpose({
? 'Masukkan pekerjaan ayah' ? 'Masukkan pekerjaan ayah'
: 'Masukkan pekerjaan' : 'Masukkan pekerjaan'
" "
:errors="errors"
/> />
</div> </div>
</div> </div>
+318 -5
View File
@@ -1,9 +1,322 @@
<script setup lang="ts"></script> <script setup lang="ts">
// type
import type { Patient, genPatientProps } from '~/models/patient'
import type { ExposedForm } from '~/types/form'
import type { PatientBase } from '~/models/patient'
// schema and models
import { genPatient } from '~/models/patient'
import { PatientSchema } from '~/schemas/patient.schema'
import { PersonAddressRelativeSchema } from '~/schemas/person-address-relative.schema'
import { PersonAddressSchema } from '~/schemas/person-address.schema'
import { PersonContactListSchema } from '~/schemas/person-contact.schema'
import { PersonFamiliesSchema } from '~/schemas/person-family.schema'
import { ResponsiblePersonSchema } from '~/schemas/person-relative.schema'
// components
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
import AppPatientEntryForm from '~/components/app/patient/entry-form.vue'
import AppPersonAddressEntryForm from '~/components/app/person-address/entry-form.vue'
import AppPersonAddressEntryFormRelative from '~/components/app/person-address/entry-form-relative.vue'
import AppPersonFamilyParentsForm from '~/components/app/person/family-parents-form.vue'
import AppPersonContactEntryForm from '~/components/app/person-contact/entry-form.vue'
import AppPersonRelativeEntryForm from '~/components/app/person-relative/entry-form.vue'
// services
import { uploadAttachment } from '~/services/patient.service'
import {
// for form entry
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleCancelForm,
} from '~/handlers/patient.handler'
import { toast } from '~/components/pub/ui/toast'
// #region Props & Emits
const props = defineProps<{
callbackUrl?: string
}>()
const residentIdentityFile = ref<File>()
const familyCardFile = ref<File>()
// form related state
const personAddressForm = ref<ExposedForm<any> | null>(null)
const personAddressRelativeForm = ref<ExposedForm<any> | null>(null)
const personContactForm = ref<ExposedForm<any> | null>(null)
const personEmergencyContactRelative = ref<ExposedForm<any> | null>(null)
const personFamilyForm = ref<ExposedForm<any> | null>(null)
const personPatientForm = ref<ExposedForm<any> | null>(null)
// #endregion
// #region State & Computed
// #endregion
// #region Lifecycle Hooks
onMounted(() => {
// Initial synchronization when forms are mounted and isSameAddress is true by default
nextTick(() => {
const isSameAddress = personAddressRelativeForm.value?.values?.isSameAddress
if (
(isSameAddress === true || isSameAddress === '1') &&
personAddressForm.value?.values &&
personAddressRelativeForm.value
) {
const currentAddressValues = personAddressForm.value.values
if (Object.keys(currentAddressValues).length > 0) {
personAddressRelativeForm.value.setValues(
{
...personAddressRelativeForm.value.values,
province_code: currentAddressValues.province_code || undefined,
regency_code: currentAddressValues.regency_code || undefined,
district_code: currentAddressValues.district_code || undefined,
village_code: currentAddressValues.village_code || undefined,
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
address: currentAddressValues.address || undefined,
rt: currentAddressValues.rt || undefined,
rw: currentAddressValues.rw || undefined,
},
false,
)
}
}
})
})
// #endregion
// #region Functions
async function composeFormData(): Promise<Patient> {
const [patient, address, addressRelative, families, contacts, emergencyContact] = await Promise.all([
personPatientForm.value?.validate(),
personAddressForm.value?.validate(),
personAddressRelativeForm.value?.validate(),
personFamilyForm.value?.validate(),
personContactForm.value?.validate(),
personEmergencyContactRelative.value?.validate(),
])
const results = [patient, address, addressRelative, families, contacts, emergencyContact]
console.log(results)
const allValid = results.every((r) => r?.valid)
// exit, if form errors happend during validation
// for example: dropdown not selected
if (!allValid) return Promise.reject('Form validation failed')
const formDataRequest: genPatientProps = {
patient: patient?.values,
residentAddress: address?.values,
cardAddress: addressRelative?.values,
familyData: families?.values,
contacts: contacts?.values,
responsible: emergencyContact?.values,
}
const formData = genPatient()
if (patient?.values.residentIdentityFile) {
residentIdentityFile.value = patient?.values.residentIdentityFile
}
if (patient?.values.familyIdentityFile) {
familyCardFile.value = patient?.values.familyIdentityFile
}
return new Promise((resolve) => resolve(formData))
}
// #endregion region
// #region Utilities & event handlers
async function handleActionClick(eventType: string) {
if (eventType === 'submit') {
const patient: Patient = await composeFormData()
let createdPatientId = 0
const response = await handleActionSave(
patient,
() => {},
() => {},
toast,
)
const data = (response?.body?.data ?? null) as PatientBase | null
if (!data) return
createdPatientId = data.id
if (residentIdentityFile.value) {
void uploadAttachment(residentIdentityFile.value, createdPatientId, 'ktp')
}
if (familyCardFile.value) {
void uploadAttachment(familyCardFile.value, createdPatientId, 'kk')
}
// If has callback provided redirect to callback with patientData
if (props.callbackUrl) {
await navigateTo(props.callbackUrl + '?patient-id=' + patient.id)
return
}
// Navigate to patient list or show success message
await navigateTo('/client/patient')
return
}
if (eventType === 'cancel') {
if (props.callbackUrl) {
await navigateTo(props.callbackUrl)
return
}
await navigateTo({
name: 'client-patient',
})
// handleCancelForm()
}
}
// #endregion
// #region Watchers
// Watcher untuk sinkronisasi initial ketika kedua form sudah ready
watch(
[() => personAddressForm.value, () => personAddressRelativeForm.value],
([addressForm, relativeForm]) => {
if (addressForm && relativeForm) {
// Trigger initial sync jika isSameAddress adalah true
nextTick(() => {
const isSameAddress = relativeForm.values?.isSameAddress
if ((isSameAddress === true || isSameAddress === '1') && addressForm.values) {
const currentAddressValues = addressForm.values
if (Object.keys(currentAddressValues).length > 0) {
relativeForm.setValues(
{
...relativeForm.values,
province_code: currentAddressValues.province_code || undefined,
regency_code: currentAddressValues.regency_code || undefined,
district_code: currentAddressValues.district_code || undefined,
village_code: currentAddressValues.village_code || undefined,
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
address: currentAddressValues.address || undefined,
rt: currentAddressValues.rt || undefined,
rw: currentAddressValues.rw || undefined,
},
false,
)
}
}
})
}
},
{ immediate: true },
)
// Watcher untuk sinkronisasi alamat ketika isSameAddress = true
watch(
() => personAddressForm.value?.values,
(newAddressValues) => {
// Cek apakah alamat KTP harus sama dengan alamat sekarang
const isSameAddress = personAddressRelativeForm.value?.values?.isSameAddress
if ((isSameAddress === true || isSameAddress === '1') && newAddressValues && personAddressRelativeForm.value) {
// Sinkronkan semua field alamat dari alamat sekarang ke alamat KTP
personAddressRelativeForm.value.setValues(
{
...personAddressRelativeForm.value.values,
province_code: newAddressValues.province_code || undefined,
regency_code: newAddressValues.regency_code || undefined,
district_code: newAddressValues.district_code || undefined,
village_code: newAddressValues.village_code || undefined,
postalRegion_code: newAddressValues.postalRegion_code || undefined,
address: newAddressValues.address || undefined,
rt: newAddressValues.rt || undefined,
rw: newAddressValues.rw || undefined,
},
false,
)
}
},
{ deep: true },
)
// Watcher untuk memantau perubahan isSameAddress
watch(
() => personAddressRelativeForm.value?.values?.isSameAddress,
(isSameAddress) => {
if (
(isSameAddress === true || isSameAddress === '1') &&
personAddressForm.value?.values &&
personAddressRelativeForm.value?.values
) {
// Ketika isSameAddress diubah menjadi true, copy alamat sekarang ke alamat KTP
const currentAddressValues = personAddressForm.value.values
personAddressRelativeForm.value.setValues(
{
...personAddressRelativeForm.value.values,
province_code: currentAddressValues.province_code || undefined,
regency_code: currentAddressValues.regency_code || undefined,
district_code: currentAddressValues.district_code || undefined,
village_code: currentAddressValues.village_code || undefined,
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
address: currentAddressValues.address || undefined,
rt: currentAddressValues.rt || undefined,
rw: currentAddressValues.rw || undefined,
},
false,
)
}
},
)
// #endregion
</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 font-semibold xl:text-xl">Tambah Pasien</div>
<Icon name="i-lucide-user" class="me-2" /> <AppPatientEntryForm
<span class="font-semibold">Tambah</span> Pasien ref="personPatientForm"
:schema="PatientSchema"
/>
<div class="h-6"></div>
<AppPersonAddressEntryForm
ref="personAddressForm"
title="Alamat Sekarang"
:schema="PersonAddressSchema"
/>
<div class="h-6"></div>
<AppPersonAddressEntryFormRelative
ref="personAddressRelativeForm"
title="Alamat KTP"
:schema="PersonAddressRelativeSchema"
/>
<div class="h-6"></div>
<AppPersonFamilyParentsForm
ref="personFamilyForm"
title="Identitas Orang Tua"
:schema="PersonFamiliesSchema"
/>
<div class="h-6"></div>
<AppPersonContactEntryForm
ref="personContactForm"
title="Kontak Pasien"
:contact-limit="10"
:schema="PersonContactListSchema"
/>
<AppPersonRelativeEntryForm
ref="personEmergencyContactRelative"
title="Penanggung Jawab"
:schema="ResponsiblePersonSchema"
/>
<div class="my-2 flex justify-end py-2">
<Action @click="handleActionClick" />
</div> </div>
<AppPatientEntryForm />
</template> </template>
<style scoped>
/* component style */
</style>
-309
View File
@@ -1,309 +0,0 @@
<script setup lang="ts">
import type { Patient, genPatientProps } from '~/models/patient'
import type { ExposedForm } from '~/types/form'
import type { PatientBase } from '~/models/patient'
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
import { genPatient } from '~/models/patient'
import { PatientSchema } from '~/schemas/patient.schema'
import { PersonAddressRelativeSchema } from '~/schemas/person-address-relative.schema'
import { PersonAddressSchema } from '~/schemas/person-address.schema'
import { PersonContactListSchema } from '~/schemas/person-contact.schema'
import { PersonFamiliesSchema } from '~/schemas/person-family.schema'
import { ResponsiblePersonSchema } from '~/schemas/person-relative.schema'
import { uploadAttachment } from '~/services/patient.service'
import {
// for form entry
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleCancelForm,
} from '~/handlers/patient.handler'
import { toast } from '~/components/pub/ui/toast'
// #region Props & Emits
const props = defineProps<{
callbackUrl?: string
}>()
const residentIdentityFile = ref<File>()
const familyCardFile = ref<File>()
// form related state
const personAddressForm = ref<ExposedForm<any> | null>(null)
const personAddressRelativeForm = ref<ExposedForm<any> | null>(null)
const personContactForm = ref<ExposedForm<any> | null>(null)
const personEmergencyContactRelative = ref<ExposedForm<any> | null>(null)
const personFamilyForm = ref<ExposedForm<any> | null>(null)
const personPatientForm = ref<ExposedForm<any> | null>(null)
// #endregion
// #region State & Computed
// #endregion
// #region Lifecycle Hooks
onMounted(() => {
// Initial synchronization when forms are mounted and isSameAddress is true by default
nextTick(() => {
const isSameAddress = personAddressRelativeForm.value?.values?.isSameAddress
if (
(isSameAddress === true || isSameAddress === '1') &&
personAddressForm.value?.values &&
personAddressRelativeForm.value
) {
const currentAddressValues = personAddressForm.value.values
if (Object.keys(currentAddressValues).length > 0) {
personAddressRelativeForm.value.setValues(
{
...personAddressRelativeForm.value.values,
province_code: currentAddressValues.province_code || undefined,
regency_code: currentAddressValues.regency_code || undefined,
district_code: currentAddressValues.district_code || undefined,
village_code: currentAddressValues.village_code || undefined,
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
address: currentAddressValues.address || undefined,
rt: currentAddressValues.rt || undefined,
rw: currentAddressValues.rw || undefined,
},
false,
)
}
}
})
})
// #endregion
// #region Functions
async function composeFormData(): Promise<Patient> {
const [patient, address, addressRelative, families, contacts, emergencyContact] = await Promise.all([
personPatientForm.value?.validate(),
personAddressForm.value?.validate(),
personAddressRelativeForm.value?.validate(),
personFamilyForm.value?.validate(),
personContactForm.value?.validate(),
personEmergencyContactRelative.value?.validate(),
])
const results = [patient, address, addressRelative, families, contacts, emergencyContact]
console.log(results)
const allValid = results.every((r) => r?.valid)
// exit, if form errors happend during validation
// for example: dropdown not selected
if (!allValid) return Promise.reject('Form validation failed')
const formDataRequest: genPatientProps = {
patient: patient?.values,
residentAddress: address?.values,
cardAddress: addressRelative?.values,
familyData: families?.values,
contacts: contacts?.values,
responsible: emergencyContact?.values,
}
const formData = genPatient(formDataRequest)
if (patient?.values.residentIdentityFile) {
residentIdentityFile.value = patient?.values.residentIdentityFile
}
if (patient?.values.familyIdentityFile) {
familyCardFile.value = patient?.values.familyIdentityFile
}
return new Promise((resolve) => resolve(formData))
}
// #endregion region
// #region Utilities & event handlers
async function handleActionClick(eventType: string) {
if (eventType === 'submit') {
const patient: Patient = await composeFormData()
let createdPatientId = 0
const response = await handleActionSave(
patient,
() => {},
() => {},
toast,
)
const data = (response?.body?.data ?? null) as PatientBase | null
if (!data) return
createdPatientId = data.id
if (residentIdentityFile.value) {
void uploadAttachment(residentIdentityFile.value, createdPatientId, 'ktp')
}
if (familyCardFile.value) {
void uploadAttachment(familyCardFile.value, createdPatientId, 'kk')
}
// If has callback provided redirect to callback with patientData
if (props.callbackUrl) {
await navigateTo(props.callbackUrl + '?patient-id=' + patient.id)
return
}
// Navigate to patient list or show success message
await navigateTo('/client/patient')
return
}
if (eventType === 'cancel') {
if (props.callbackUrl) {
await navigateTo(props.callbackUrl)
return
}
await navigateTo({
name: 'client-patient',
})
// handleCancelForm()
}
}
// #endregion
// #region Watchers
// Watcher untuk sinkronisasi initial ketika kedua form sudah ready
watch(
[() => personAddressForm.value, () => personAddressRelativeForm.value],
([addressForm, relativeForm]) => {
if (addressForm && relativeForm) {
// Trigger initial sync jika isSameAddress adalah true
nextTick(() => {
const isSameAddress = relativeForm.values?.isSameAddress
if ((isSameAddress === true || isSameAddress === '1') && addressForm.values) {
const currentAddressValues = addressForm.values
if (Object.keys(currentAddressValues).length > 0) {
relativeForm.setValues(
{
...relativeForm.values,
province_code: currentAddressValues.province_code || undefined,
regency_code: currentAddressValues.regency_code || undefined,
district_code: currentAddressValues.district_code || undefined,
village_code: currentAddressValues.village_code || undefined,
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
address: currentAddressValues.address || undefined,
rt: currentAddressValues.rt || undefined,
rw: currentAddressValues.rw || undefined,
},
false,
)
}
}
})
}
},
{ immediate: true },
)
// Watcher untuk sinkronisasi alamat ketika isSameAddress = true
watch(
() => personAddressForm.value?.values,
(newAddressValues) => {
// Cek apakah alamat KTP harus sama dengan alamat sekarang
const isSameAddress = personAddressRelativeForm.value?.values?.isSameAddress
if ((isSameAddress === true || isSameAddress === '1') && newAddressValues && personAddressRelativeForm.value) {
// Sinkronkan semua field alamat dari alamat sekarang ke alamat KTP
personAddressRelativeForm.value.setValues(
{
...personAddressRelativeForm.value.values,
province_code: newAddressValues.province_code || undefined,
regency_code: newAddressValues.regency_code || undefined,
district_code: newAddressValues.district_code || undefined,
village_code: newAddressValues.village_code || undefined,
postalRegion_code: newAddressValues.postalRegion_code || undefined,
address: newAddressValues.address || undefined,
rt: newAddressValues.rt || undefined,
rw: newAddressValues.rw || undefined,
},
false,
)
}
},
{ deep: true },
)
// Watcher untuk memantau perubahan isSameAddress
watch(
() => personAddressRelativeForm.value?.values?.isSameAddress,
(isSameAddress) => {
if (
(isSameAddress === true || isSameAddress === '1') &&
personAddressForm.value?.values &&
personAddressRelativeForm.value?.values
) {
// Ketika isSameAddress diubah menjadi true, copy alamat sekarang ke alamat KTP
const currentAddressValues = personAddressForm.value.values
personAddressRelativeForm.value.setValues(
{
...personAddressRelativeForm.value.values,
province_code: currentAddressValues.province_code || undefined,
regency_code: currentAddressValues.regency_code || undefined,
district_code: currentAddressValues.district_code || undefined,
village_code: currentAddressValues.village_code || undefined,
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
address: currentAddressValues.address || undefined,
rt: currentAddressValues.rt || undefined,
rw: currentAddressValues.rw || undefined,
},
false,
)
}
},
)
// #endregion
</script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Tambah Pasien</div>
<AppPatientEntryForm
ref="personPatientForm"
:schema="PatientSchema"
/>
<div class="h-6"></div>
<AppPersonAddressEntryForm
ref="personAddressForm"
title="Alamat Sekarang"
:schema="PersonAddressSchema"
/>
<div class="h-6"></div>
<AppPersonAddressEntryFormRelative
ref="personAddressRelativeForm"
title="Alamat KTP"
:schema="PersonAddressRelativeSchema"
/>
<div class="h-6"></div>
<AppPersonFamilyParentsForm
ref="personFamilyForm"
title="Identitas Orang Tua"
:schema="PersonFamiliesSchema"
/>
<div class="h-6"></div>
<AppPersonContactEntryForm
ref="personContactForm"
title="Kontak Pasien"
:contact-limit="10"
:schema="PersonContactListSchema"
/>
<AppPersonRelativeEntryForm
ref="personEmergencyContactRelative"
title="Penanggung Jawab"
:schema="ResponsiblePersonSchema"
/>
<div class="my-2 flex justify-end py-2">
<Action @click="handleActionClick" />
</div>
</template>
<style scoped>
/* component style */
</style>
+1 -1
View File
@@ -34,7 +34,7 @@ const callbackUrl = route.query['return-path'] as string | undefined
<template> <template>
<div> <div>
<div v-if="canRead"> <div v-if="canRead">
<ContentPatientEntry :callback-url="callbackUrl" /> <ContentPatientAdd :callback-url="callbackUrl" />
</div> </div>
<Error <Error
v-else v-else