edit-mode: unwrap detail data and use strict type

refactor(patient-form): simplify address synchronization logic

- Extract address sync logic into helper functions
- Remove unused schema imports
- Streamline mounted hook with new loadInitData function
- Consolidate watchers into single efficient watcher

feat(forms): add readonly support across all form components

Implement readonly state handling for patient, relative, contact, and address forms
Add isDisabled prop to all form fields to support readonly mode
Update form components to respect isReadonly prop from parent

refactor(patient-form): reorganize imports and improve type usage

- Group related imports and move type imports to the top
- Replace genPatient with genPatientEntity for better type safety
- Remove console.log statement
- Fix formatting and indentation issues
This commit is contained in:
Khafid Prayoga
2025-12-06 09:13:28 +07:00
parent 1f6ca8a7f9
commit 05e2f32197
18 changed files with 392 additions and 177 deletions
+251 -137
View File
@@ -1,20 +1,13 @@
<script setup lang="ts">
// type
import { withBase } from '~/models/_base'
import type { PatientEntity, PatientBase, Patient, genPatientProps } from '~/models/patient'
import type { Person } from '~/models/person'
import type { PersonAddress } from '~/models/person-address'
import type { PersonContact } from '~/models/person-contact'
import type { PersonRelative } from '~/models/person-relative'
import type { ExposedForm } from '~/types/form'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
// 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'
import type { PatientEntity, PatientBase, Patient, genPatientProps } from '~/models/patient'
import { genPatientEntity } from '~/models/patient'
// components
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
@@ -29,7 +22,6 @@ import AppPersonRelativeEntryForm from '~/components/app/person-relative/entry-f
import { getPatientDetail, uploadAttachment } from '~/services/patient.service'
import {
// for form entry
isReadonly,
isProcessing,
isFormEntryDialogOpen,
@@ -41,6 +33,14 @@ import {
import { toast } from '~/components/pub/ui/toast'
// reverse mapping untuk contact type (backend → UI)
const reverseContactTypeMapping: Record<string, string> = {
'm-phone': 'phoneNumber',
phone: 'homePhoneNumber',
email: 'email',
fax: 'fax',
}
// #region Props & Emits
const props = defineProps<{
callbackUrl?: string
@@ -70,51 +70,178 @@ const patient = ref(
personRelatives: [],
}),
)
// Key untuk memaksa re-render form saat data berubah
const formKey = ref(0)
// Computed: unwrap patient data untuk form patient
const patientFormInitialValues = computed(() => {
const p = patient.value
const person = p.person
if (!person || !person.name) return undefined
return {
identityNumber: person.residentIdentityNumber || '',
drivingLicenseNumber: person.drivingLicenseNumber || '',
passportNumber: person.passportNumber || '',
fullName: person.name || '',
isNewBorn: (p.newBornStatus ? 'YA' : 'TIDAK') as 'YA' | 'TIDAK',
gender: person.gender_code || '',
birthPlace: person.birthRegency_code || '',
birthDate: person.birthDate ? new Date(person.birthDate).toISOString().split('T')[0] : '',
education: person.education_code || '',
job: person.occupation_code || '',
maritalStatus: '', // perlu mapping jika ada field ini
nationality: person.nationality || 'WNI',
ethnicity: person.ethnic_code || '',
language: person.language_code || '',
religion: person.religion_code || '',
communicationBarrier: (person.communicationIssueStatus ? 'YA' : 'TIDAK') as 'YA' | 'TIDAK',
disability: (person.disability ? 'YA' : 'TIDAK') as 'YA' | 'TIDAK',
disabilityType: person.disability || '',
note: '',
}
})
// Computed: unwrap alamat domisili (alamat sekarang)
const addressFormInitialValues = computed(() => {
const addresses = patient.value.person?.addresses || patient.value.personAddresses || []
const domicileAddress = addresses.find((a: PersonAddress) => a.locationType_code === 'domicile')
if (!domicileAddress) return undefined
// extract kode wilayah dari preload data
const village = domicileAddress.postalRegion?.village
const district = village?.district
const regency = district?.regency
const province = regency?.province
return {
locationType_code: 'domicile',
province_code: province?.code || '',
regency_code: regency?.code || '',
district_code: district?.code || '',
village_code: village?.code || domicileAddress.village_code || '',
postalRegion_code: domicileAddress.postalRegion_code || '',
address: domicileAddress.address || '',
rt: domicileAddress.rt || '',
rw: domicileAddress.rw || '',
}
})
// Computed: unwrap alamat KTP (identity)
const addressRelativeFormInitialValues = computed(() => {
const addresses = patient.value.person?.addresses || patient.value.personAddresses || []
const domicileAddress = addresses.find((a: PersonAddress) => a.locationType_code === 'domicile')
const identityAddress = addresses.find((a: PersonAddress) => a.locationType_code === 'identity')
// Jika tidak ada alamat KTP terpisah, berarti sama dengan domisili
if (!identityAddress) {
return {
isSameAddress: '1',
locationType_code: 'identity',
}
}
// Cek apakah alamat sama dengan domisili
const isSame =
domicileAddress &&
identityAddress.village_code === domicileAddress.village_code &&
identityAddress.address === domicileAddress.address
if (isSame) {
return {
isSameAddress: '1',
locationType_code: 'identity',
}
}
// extract kode wilayah dari preload data
const village = identityAddress.postalRegion?.village
const district = village?.district
const regency = district?.regency
const province = regency?.province
return {
isSameAddress: '0',
locationType_code: 'identity',
province_code: province?.code || '',
regency_code: regency?.code || '',
district_code: district?.code || '',
village_code: village?.code || identityAddress.village_code || '',
postalRegion_code: identityAddress.postalRegion_code || '',
address: identityAddress.address || '',
rt: identityAddress.rt || '',
rw: identityAddress.rw || '',
}
})
// Computed: unwrap data orang tua
const familyFormInitialValues = computed(() => {
const relatives = patient.value.person?.relatives || patient.value.personRelatives || []
const parents = relatives.filter(
(r: PersonRelative) => !r.responsible && (r.relationship_code === 'mother' || r.relationship_code === 'father'),
)
if (parents.length === 0) {
return {
shareFamilyData: '0',
families: [],
}
}
return {
shareFamilyData: '1',
families: parents.map((parent: PersonRelative) => ({
relation: parent.relationship_code || '',
name: parent.name || '',
education: parent.education_code || '',
occupation: parent.occupation_name || parent.occupation_code || '',
})),
}
})
// Computed: unwrap kontak pasien
const contactFormInitialValues = computed(() => {
const contacts = patient.value.person?.contacts || patient.value.personContacts || []
if (contacts.length === 0) return undefined
return {
contacts: contacts.map((contact: PersonContact) => ({
contactType: reverseContactTypeMapping[contact.type_code] || contact.type_code || '',
contactNumber: contact.value || '',
})),
}
})
// Computed: unwrap penanggung jawab
const responsibleFormInitialValues = computed(() => {
const relatives = patient.value.person?.relatives || patient.value.personRelatives || []
const responsibles = relatives.filter((r: PersonRelative) => r.responsible === true)
if (responsibles.length === 0) return undefined
return {
contacts: responsibles.map((r: PersonRelative) => ({
relation: r.relationship_code || '',
name: r.name || '',
address: r.address || '',
phone: r.phoneNumber || '',
})),
}
})
// #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,
)
}
}
})
onMounted(async () => {
// if edit mode, fetch patient detail
if (props.mode === 'edit' && props.patientId) {
void getPatientDetail(props.patientId as number).then((v) => {
if (v.success) {
patient.value = v.body.data || {}
}
})
await loadInitData(props.patientId)
}
})
// #endregion
// #region Functions
async function composeFormData(): Promise<Patient> {
async function composeFormData(): Promise<PatientEntity> {
const [patient, address, addressRelative, families, contacts, emergencyContact] = await Promise.all([
personPatientForm.value?.validate(),
personAddressForm.value?.validate(),
@@ -141,7 +268,7 @@ async function composeFormData(): Promise<Patient> {
responsible: emergencyContact?.values,
}
const formData = genPatient()
const formData = genPatientEntity(formDataRequest)
if (patient?.values.residentIdentityFile) {
residentIdentityFile.value = patient?.values.residentIdentityFile
@@ -156,6 +283,20 @@ async function composeFormData(): Promise<Patient> {
// #endregion region
// #region Utilities & event handlers
async function loadInitData(id: number | string) {
isProcessing.value = true
try {
const response = await getPatientDetail(id as number)
if (response.success) {
patient.value = response.body.data || {}
// Increment key untuk memaksa re-render form dengan data baru
formKey.value++
}
} finally {
isProcessing.value = false
}
}
async function handleActionClick(eventType: string) {
try {
if (eventType === 'submit') {
@@ -197,10 +338,12 @@ async function handleActionClick(eventType: string) {
return
}
// handleCancelForm()
}
if (eventType === 'back') {
await navigateTo({
name: 'client-patient',
})
// handleCancelForm()
}
} catch (error) {
// Show error toast to user
@@ -228,124 +371,95 @@ async function handleActionClick(eventType: string) {
// #endregion
// #region Watchers
// Watcher untuk sinkronisasi initial ketika kedua form sudah ready
// Helper: Cek apakah isSameAddress aktif
const isSameAddressActive = () => {
const val = personAddressRelativeForm.value?.values?.isSameAddress
return val === true || val === '1'
}
// Helper: Sinkronkan alamat sekarang ke alamat KTP
const syncAddressToRelative = () => {
const source = personAddressForm.value?.values
const target = personAddressRelativeForm.value
if (!source || !target) return
const addressFields = [
'province_code',
'regency_code',
'district_code',
'village_code',
'postalRegion_code',
'address',
'rt',
'rw',
] as const
const syncedValues = Object.fromEntries(addressFields.map((key) => [key, source[key] || undefined]))
target.setValues({ ...target.values, ...syncedValues }, false)
}
// Watcher: Sinkronisasi saat nilai alamat berubah atau isSameAddress diaktifkan
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,
)
[() => personAddressForm.value?.values, () => personAddressRelativeForm.value?.values?.isSameAddress],
() => {
if (isSameAddressActive()) {
syncAddressToRelative()
}
},
{ deep: true, immediate: true },
)
// #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" />
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">
{{ mode === 'edit' ? 'Edit Pasien' : 'Tambah Pasien' }}
</div>
<AppPatientEntryForm
:key="`patient-${formKey}`"
ref="personPatientForm"
:is-readonly="isProcessing || isReadonly"
:initial-values="patientFormInitialValues"
/>
<div class="h-6"></div>
<AppPersonAddressEntryForm
:key="`address-${formKey}`"
ref="personAddressForm"
title="Alamat Sekarang"
:is-readonly="isProcessing || isReadonly"
:initial-values="addressFormInitialValues"
/>
<div class="h-6"></div>
<AppPersonAddressEntryFormRelative
:key="`address-rel-${formKey}`"
ref="personAddressRelativeForm"
title="Alamat KTP"
:is-readonly="isProcessing || isReadonly"
:initial-values="addressRelativeFormInitialValues"
/>
<div class="h-6"></div>
<AppPersonFamilyParentsForm
:key="`family-${formKey}`"
ref="personFamilyForm"
title="Identitas Orang Tua"
:is-readonly="isProcessing || isReadonly"
:initial-values="familyFormInitialValues"
/>
<div class="h-6"></div>
<AppPersonContactEntryForm
:key="`contact-${formKey}`"
ref="personContactForm"
title="Kontak Pasien"
:is-readonly="isProcessing || isReadonly"
:initial-values="contactFormInitialValues"
/>
<AppPersonRelativeEntryForm
:key="`responsible-${formKey}`"
ref="personEmergencyContactRelative"
title="Penanggung Jawab"
:is-readonly="isProcessing || isReadonly"
:initial-values="responsibleFormInitialValues"
/>
<div class="my-2 flex justify-end py-2">