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 10:52:35 +07:00
parent 1f6ca8a7f9
commit 05e2f32197
18 changed files with 392 additions and 177 deletions

No files matched your search

+50 -4
View File
@@ -27,8 +27,35 @@ import {
} from './fields' } from './fields'
interface FormData extends PatientFormData {} interface FormData extends PatientFormData {}
// Type untuk initial values (sebelum transform schema)
interface PatientFormInput {
identityNumber?: string
drivingLicenseNumber?: string
passportNumber?: string
fullName?: string
isNewBorn?: 'YA' | 'TIDAK'
gender?: string
birthPlace?: string
birthDate?: string
education?: string
job?: string
maritalStatus?: string
nationality?: string
ethnicity?: string
language?: string
religion?: string
communicationBarrier?: 'YA' | 'TIDAK'
disability?: 'YA' | 'TIDAK'
disabilityType?: string
note?: string
residentIdentityFile?: File
familyIdentityFile?: File
}
interface Props { interface Props {
initialValues?: FormData isReadonly: boolean
initialValues?: PatientFormInput
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -37,7 +64,7 @@ const formSchema = toTypedSchema(PatientSchema)
const { values, resetForm, setValues, validate } = useForm<FormData>({ const { values, resetForm, setValues, validate } = useForm<FormData>({
name: 'patientForm', name: 'patientForm',
validationSchema: formSchema, validationSchema: formSchema,
initialValues: props.initialValues && {}, initialValues: (props.initialValues ?? {}) as any,
validateOnMount: false, validateOnMount: false,
}) })
@@ -61,6 +88,7 @@ defineExpose({
label="No. KTP" label="No. KTP"
placeholder="Masukkan NIK" placeholder="Masukkan NIK"
numeric-only numeric-only
:is-disabled="isReadonly"
/> />
<InputBase <InputBase
field-name="drivingLicenseNumber" field-name="drivingLicenseNumber"
@@ -68,12 +96,14 @@ defineExpose({
placeholder="Masukkan nomor SIM" placeholder="Masukkan nomor SIM"
numeric-only numeric-only
:max-length="20" :max-length="20"
:is-disabled="isReadonly"
/> />
<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"
:is-disabled="isReadonly"
/> />
<InputName <InputName
field-name-alias="alias" field-name-alias="alias"
@@ -81,46 +111,54 @@ defineExpose({
label-for-input="Nama Lengkap" label-for-input="Nama Lengkap"
placeholder="Masukkan nama lengkap pasien" placeholder="Masukkan nama lengkap pasien"
is-required is-required
:is-disabled="isReadonly"
/> />
<RadioNewborn <RadioNewborn
field-name="isNewBorn" field-name="isNewBorn"
label="Pasien Bayi" label="Pasien Bayi"
placeholder="Pilih status pasien" placeholder="Pilih status pasien"
is-required is-required
:is-disabled="isReadonly"
/> />
<SelectGender <SelectGender
field-name="gender" field-name="gender"
label="Jenis Kelamin" label="Jenis Kelamin"
placeholder="Pilih jenis kelamin" placeholder="Pilih jenis kelamin"
is-required is-required
:is-disabled="isReadonly"
/> />
<SelectBirthPlace <SelectBirthPlace
field-name="birthPlace" field-name="birthPlace"
label="Tempat Lahir" label="Tempat Lahir"
placeholder="Pilih tempat lahir" placeholder="Pilih tempat lahir"
is-required is-required
:is-disabled="isReadonly"
/> />
<SelectDob <SelectDob
label="Tanggal Lahir" label="Tanggal Lahir"
is-required is-required
:is-disabled="isReadonly"
/> />
<SelectEducation <SelectEducation
field-name="education" field-name="education"
label="Pendidikan" label="Pendidikan"
placeholder="Pilih pendidikan" placeholder="Pilih pendidikan"
is-required is-required
:is-disabled="isReadonly"
/> />
<SelectJob <SelectJob
field-name="job" field-name="job"
label="Pekerjaan" label="Pekerjaan"
placeholder="Pilih pekerjaan" placeholder="Pilih pekerjaan"
is-required is-required
:is-disabled="isReadonly"
/> />
<SelectMaritalStatus <SelectMaritalStatus
field-name="maritalStatus" field-name="maritalStatus"
label="Status Perkawinan" label="Status Perkawinan"
placeholder="Pilih status Perkawinan" placeholder="Pilih status Perkawinan"
is-required is-required
:is-disabled="isReadonly"
/> />
<DE.Cell /> <DE.Cell />
<RadioNationality <RadioNationality
@@ -128,45 +166,51 @@ defineExpose({
label="Kebangsaan" label="Kebangsaan"
placeholder="Pilih kebangsaan" placeholder="Pilih kebangsaan"
is-required is-required
:is-disabled="isReadonly"
/> />
<SelectEthnicity <SelectEthnicity
field-name="ethnicity" field-name="ethnicity"
label="Suku" label="Suku"
placeholder="Pilih suku bangsa" placeholder="Pilih suku bangsa"
:is-disabled="values.nationality !== 'WNI'" :is-disabled="isReadonly || values.nationality !== 'WNI'"
/> />
<SelectLanguage <SelectLanguage
field-name="language" field-name="language"
label="Bahasa" label="Bahasa"
placeholder="Pilih preferensi bahasa" placeholder="Pilih preferensi bahasa"
is-required is-required
:is-disabled="isReadonly"
/> />
<SelectReligion <SelectReligion
field-name="religion" field-name="religion"
label="Agama" label="Agama"
placeholder="Pilih agama" placeholder="Pilih agama"
is-required is-required
:is-disabled="isReadonly"
/> />
<RadioCommunicationBarrier <RadioCommunicationBarrier
field-name="communicationBarrier" field-name="communicationBarrier"
label="Hambatan Berkomunikasi" label="Hambatan Berkomunikasi"
is-required is-required
:is-disabled="isReadonly"
/> />
<RadioDisability <RadioDisability
field-name="disability" field-name="disability"
label="Disabilitas" label="Disabilitas"
is-required is-required
:is-disabled="isReadonly"
/> />
<SelectDisability <SelectDisability
label="Jenis Disabilitas" label="Jenis Disabilitas"
field-name="disabilityType" field-name="disabilityType"
:is-disabled="values.disability !== 'YA'" :is-disabled="isReadonly || values.disability !== 'YA'"
:is-required="values.disability === 'YA'" :is-required="values.disability === 'YA'"
/> />
<InputBase <InputBase
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"
:is-disabled="isReadonly"
/> />
</DE.Block> </DE.Block>
@@ -183,6 +227,7 @@ defineExpose({
placeholder="Unggah scan dokumen KTP" placeholder="Unggah scan dokumen KTP"
:accept="['pdf', 'jpg', 'png']" :accept="['pdf', 'jpg', 'png']"
:max-size-mb="1" :max-size-mb="1"
:is-disabled="isReadonly"
/> />
<FileUpload <FileUpload
field-name="familyCardFile" field-name="familyCardFile"
@@ -190,6 +235,7 @@ defineExpose({
placeholder="Unggah scan dokumen KK" placeholder="Unggah scan dokumen KK"
:accept="['pdf', 'jpg', 'png']" :accept="['pdf', 'jpg', 'png']"
:max-size-mb="1" :max-size-mb="1"
:is-disabled="isReadonly"
/> />
</DE.Block> </DE.Block>
</form> </form>
@@ -17,6 +17,7 @@ defineProps<{
labelClass?: string labelClass?: string
maxLength?: number maxLength?: number
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean
}>() }>()
</script> </script>
@@ -40,6 +41,7 @@ defineProps<{
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input
:disabled="isDisabled"
v-bind="componentField" v-bind="componentField"
type="text" type="text"
:placeholder="placeholder" :placeholder="placeholder"
@@ -15,6 +15,7 @@ const props = defineProps<{
radioItemClass?: string radioItemClass?: string
labelClass?: string labelClass?: string
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean
}>() }>()
const { const {
@@ -55,6 +56,7 @@ const genderOptions = [
<FormControl> <FormControl>
<RadioGroup <RadioGroup
v-bind="componentField" v-bind="componentField"
:disabled="isDisabled"
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)" :class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
> >
<div <div
@@ -63,20 +65,24 @@ const genderOptions = [
:class="cn('flex min-w-fit items-center space-x-2', radioItemClass)" :class="cn('flex min-w-fit items-center space-x-2', radioItemClass)"
> >
<RadioGroupItem <RadioGroupItem
:disabled="isDisabled"
:id="`${fieldName}-${index}`" :id="`${fieldName}-${index}`"
:value="option.value" :value="option.value"
:class=" :class="
cn( cn(
'relative h-4 w-4 rounded-full border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5', 'peer relative h-4 w-4 rounded-full border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
containerClass, containerClass,
) )
" "
/> />
<RadioLabel <RadioLabel
:for="`${fieldName}-${index}`" :for="isDisabled ? undefined : `${fieldName}-${index}`"
:class=" :class="
cn( cn(
'cursor-pointer select-none font-normal text-xs leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm', 'select-none text-xs !font-normal leading-none transition-colors sm:text-sm',
isDisabled
? 'cursor-not-allowed opacity-70'
: 'cursor-pointer hover:text-primary',
labelClass, labelClass,
) )
" "
@@ -15,6 +15,7 @@ const props = defineProps<{
radioItemClass?: string radioItemClass?: string
labelClass?: string labelClass?: string
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean
}>() }>()
const { const {
@@ -55,6 +56,7 @@ const dissabilityOptions = [
<FormControl> <FormControl>
<RadioGroup <RadioGroup
v-bind="componentField" v-bind="componentField"
:disabled="isDisabled"
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)" :class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
> >
<div <div
@@ -63,20 +65,24 @@ const dissabilityOptions = [
:class="cn('flex min-w-fit items-center space-x-2', radioItemClass)" :class="cn('flex min-w-fit items-center space-x-2', radioItemClass)"
> >
<RadioGroupItem <RadioGroupItem
:disabled="isDisabled"
:id="`${fieldName}-${index}`" :id="`${fieldName}-${index}`"
:value="option.value" :value="option.value"
:class=" :class="
cn( cn(
'relative h-4 w-4 rounded-full border-1 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5', 'peer border-1 relative h-4 w-4 rounded-full border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
containerClass, containerClass,
) )
" "
/> />
<RadioLabel <RadioLabel
:for="`${fieldName}-${index}`" :for="isDisabled ? undefined : `${fieldName}-${index}`"
:class=" :class="
cn( cn(
'cursor-pointer select-none text-xs !font-normal leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm', 'select-none text-xs !font-normal leading-none transition-colors',
isDisabled
? 'cursor-not-allowed opacity-70'
: 'cursor-pointer hover:text-primary',
labelClass, labelClass,
) )
" "
@@ -15,6 +15,7 @@ const props = defineProps<{
radioItemClass?: string radioItemClass?: string
labelClass?: string labelClass?: string
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean
}>() }>()
const { const {
@@ -55,6 +56,7 @@ const nationalityOptions = [
<FormControl> <FormControl>
<RadioGroup <RadioGroup
v-bind="componentField" v-bind="componentField"
:disabled="isDisabled"
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)" :class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
> >
<div <div
@@ -63,20 +65,24 @@ const nationalityOptions = [
:class="cn('flex min-w-fit items-center space-x-2', radioItemClass)" :class="cn('flex min-w-fit items-center space-x-2', radioItemClass)"
> >
<RadioGroupItem <RadioGroupItem
:disabled="isDisabled"
:id="`${fieldName}-${index}`" :id="`${fieldName}-${index}`"
:value="option.value" :value="option.value"
:class=" :class="
cn( cn(
'relative h-4 w-4 rounded-full border-1 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5', 'peer relative h-4 w-4 rounded-full border-1 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
containerClass, containerClass,
) )
" "
/> />
<RadioLabel <RadioLabel
:for="`${fieldName}-${index}`" :for="isDisabled ? undefined : `${fieldName}-${index}`"
:class=" :class="
cn( cn(
'cursor-pointer select-none text-xs !font-normal leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm', 'select-none text-xs !font-normal leading-none transition-colors sm:text-sm',
isDisabled
? 'cursor-not-allowed opacity-70'
: 'cursor-pointer hover:text-primary',
labelClass, labelClass,
) )
" "
@@ -15,6 +15,7 @@ const props = defineProps<{
radioItemClass?: string radioItemClass?: string
labelClass?: string labelClass?: string
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean
}>() }>()
const { const {
@@ -34,7 +35,10 @@ const newbornOptions = [
</script> </script>
<template> <template>
<DE.Cell :class="cn('radio-group-field', containerClass)" :col-span="2"> <DE.Cell
:class="cn('radio-group-field', containerClass)"
:col-span="2"
>
<DE.Label <DE.Label
:label-for="fieldName" :label-for="fieldName"
:is-required="isRequired" :is-required="isRequired"
@@ -52,6 +56,7 @@ const newbornOptions = [
<FormItem> <FormItem>
<FormControl> <FormControl>
<RadioGroup <RadioGroup
:disabled="isDisabled"
v-bind="componentField" v-bind="componentField"
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)" :class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
> >
@@ -61,20 +66,24 @@ const newbornOptions = [
:class="cn('flex min-w-fit items-center space-x-2 pt-1', radioItemClass)" :class="cn('flex min-w-fit items-center space-x-2 pt-1', radioItemClass)"
> >
<RadioGroupItem <RadioGroupItem
:disabled="isDisabled"
:id="`${fieldName}-${index}`" :id="`${fieldName}-${index}`"
:value="option.value" :value="option.value"
:class=" :class="
cn( cn(
'relative h-4 w-4 rounded-full border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5', 'peer relative h-4 w-4 rounded-full border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
containerClass, containerClass,
) )
" "
/> />
<RadioLabel <RadioLabel
:for="`${fieldName}-${index}`" :for="isDisabled ? undefined : `${fieldName}-${index}`"
:class=" :class="
cn( cn(
'cursor-pointer select-none text-xs font-normal leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm', 'select-none text-xs !font-normal leading-none transition-colors',
isDisabled
? 'cursor-not-allowed opacity-70'
: 'cursor-pointer hover:text-primary',
labelClass, labelClass,
) )
" "
@@ -16,6 +16,7 @@ const props = defineProps<{
fieldGroupClass?: string fieldGroupClass?: string
labelClass?: string labelClass?: string
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean
}>() }>()
const { const {
@@ -95,6 +96,7 @@ function calculateAge(birthDate: string | Date | undefined): string {
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input
:disabled="isDisabled"
id="birthDate" id="birthDate"
type="date" type="date"
min="1900-01-01" min="1900-01-01"
@@ -16,6 +16,7 @@ const props = defineProps<{
fieldGroupClass?: string fieldGroupClass?: string
labelClass?: string labelClass?: string
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean
}>() }>()
const { const {
@@ -53,8 +54,9 @@ const jobOptions = mapToComboboxOptList(occupationCodes)
<FormItem> <FormItem>
<FormControl> <FormControl>
<Combobox <Combobox
:id="fieldName"
v-bind="componentField" v-bind="componentField"
:is-disabled="isDisabled"
:id="fieldName"
:items="jobOptions" :items="jobOptions"
:placeholder="placeholder" :placeholder="placeholder"
search-placeholder="Cari..." search-placeholder="Cari..."
@@ -15,6 +15,7 @@ const props = defineProps<{
fieldGroupClass?: string fieldGroupClass?: string
labelClass?: string labelClass?: string
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean
}>() }>()
const { const {
@@ -60,6 +61,7 @@ const langOptions = [
<FormItem> <FormItem>
<FormControl> <FormControl>
<Select <Select
:is-disabled="isDisabled"
:id="fieldName" :id="fieldName"
v-bind="componentField" v-bind="componentField"
:items="langOptions" :items="langOptions"
@@ -14,6 +14,7 @@ const props = defineProps<{
fieldGroupClass?: string fieldGroupClass?: string
labelClass?: string labelClass?: string
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean
placeholder?: string placeholder?: string
}>() }>()
@@ -56,8 +57,9 @@ const maritalStatusOptions = [
<FormItem> <FormItem>
<FormControl> <FormControl>
<Select <Select
:id="fieldName"
v-bind="componentField" v-bind="componentField"
:is-disabled="isDisabled"
:id="fieldName"
:items="maritalStatusOptions" :items="maritalStatusOptions"
:placeholder="placeholder" :placeholder="placeholder"
:preserve-order="true" :preserve-order="true"
@@ -16,6 +16,7 @@ const props = defineProps<{
fieldGroupClass?: string fieldGroupClass?: string
labelClass?: string labelClass?: string
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean
}>() }>()
const { const {
@@ -68,6 +69,7 @@ const religionOptions = Array.from(
<FormItem> <FormItem>
<FormControl> <FormControl>
<Select <Select
:is-disabled="isDisabled"
:id="fieldName" :id="fieldName"
v-bind="componentField" v-bind="componentField"
:items="religionOptions" :items="religionOptions"
@@ -19,6 +19,7 @@ interface FormData extends PersonAddressRelativeFormData {}
interface Props { interface Props {
title: string title: string
isReadonly: boolean
conf?: { conf?: {
withAddressName?: boolean withAddressName?: boolean
} }
@@ -313,6 +314,7 @@ watch(
class="flex min-w-fit items-center space-x-2" class="flex min-w-fit items-center space-x-2"
> >
<RadioGroupItem <RadioGroupItem
:disabled="isReadonly"
:id="`isSameAddress-${index}`" :id="`isSameAddress-${index}`"
:value="option.value" :value="option.value"
class="relative h-4 w-4 rounded-full border-2 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5" class="relative h-4 w-4 rounded-full border-2 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5"
@@ -334,32 +336,32 @@ watch(
<SelectProvince <SelectProvince
field-name="province_code" field-name="province_code"
placeholder="Pilih" placeholder="Pilih"
:is-disabled="isSameAddress" :is-disabled="isReadonly || isSameAddress"
:is-required="!isSameAddress" :is-required="!isSameAddress"
/> />
<SelectRegency <SelectRegency
field-name="regency_code" field-name="regency_code"
:province-code="values.province_code" :province-code="values.province_code"
:is-disabled="getFieldState('regency_code').disabled" :is-disabled="isReadonly || getFieldState('regency_code').disabled"
:is-required="!isSameAddress" :is-required="!isSameAddress"
/> />
<SelectDistrict <SelectDistrict
field-name="district_code" field-name="district_code"
:regency-code="values.regency_code" :regency-code="values.regency_code"
:is-disabled="getFieldState('district_code').disabled" :is-disabled="isReadonly || getFieldState('district_code').disabled"
:is-required="!isSameAddress" :is-required="!isSameAddress"
/> />
<SelectVillage <SelectVillage
field-name="village_code" field-name="village_code"
:district-code="values.district_code" :district-code="values.district_code"
:is-disabled="getFieldState('village_code').disabled" :is-disabled="isReadonly || getFieldState('village_code').disabled"
:is-required="!isSameAddress" :is-required="!isSameAddress"
/> />
<InputBase <InputBase
field-name="address" field-name="address"
label="Alamat" label="Alamat"
:placeholder="getFieldState('address').placeholder" :placeholder="getFieldState('address').placeholder"
:is-disabled="getFieldState('address').disabled" :is-disabled="isReadonly || getFieldState('address').disabled"
:is-required="!isSameAddress" :is-required="!isSameAddress"
:col-span="2" :col-span="2"
/> />
@@ -370,13 +372,13 @@ watch(
numeric-only numeric-only
:max-length="2" :max-length="2"
:placeholder="getFieldState('rt').placeholder" :placeholder="getFieldState('rt').placeholder"
:is-disabled="getFieldState('rt').disabled" :is-disabled="isReadonly || getFieldState('rt').disabled"
/> />
<InputBase <InputBase
field-name="rw" field-name="rw"
label="RW" label="RW"
:placeholder="getFieldState('rw').placeholder" :placeholder="getFieldState('rw').placeholder"
:is-disabled="getFieldState('rw').disabled" :is-disabled="isReadonly || getFieldState('rw').disabled"
:max-length="2" :max-length="2"
numeric-only numeric-only
/> />
@@ -385,7 +387,7 @@ watch(
field-name="postalRegion_code" field-name="postalRegion_code"
:village-code="values.village_code" :village-code="values.village_code"
:placeholder="getFieldState('postalRegion_code').placeholder" :placeholder="getFieldState('postalRegion_code').placeholder"
:is-disabled="getFieldState('postalRegion_code').disabled || !values.village_code" :is-disabled="isReadonly || getFieldState('postalRegion_code').disabled || !values.village_code"
/> />
</DE.Block> </DE.Block>
</form> </form>
@@ -15,6 +15,7 @@ interface FormData extends PersonAddressFormData {}
interface Props { interface Props {
title: string title: string
isReadonly: boolean
conf?: { conf?: {
withAddressName?: boolean withAddressName?: boolean
} }
@@ -188,21 +189,25 @@ watch(
<SelectProvince <SelectProvince
field-name="province_code" field-name="province_code"
placeholder="Pilih" placeholder="Pilih"
:is-disabled="isReadonly"
is-required is-required
/> />
<SelectRegency <SelectRegency
field-name="regency_code" field-name="regency_code"
:province-code="values.province_code" :province-code="values.province_code"
:is-disabled="isReadonly"
is-required is-required
/> />
<SelectDistrict <SelectDistrict
field-name="district_code" field-name="district_code"
:regency-code="values.regency_code" :regency-code="values.regency_code"
:is-disabled="isReadonly"
is-required is-required
/> />
<SelectVillage <SelectVillage
field-name="village_code" field-name="village_code"
:district-code="values.district_code" :district-code="values.district_code"
:is-disabled="isReadonly"
is-required is-required
/> />
<InputBase <InputBase
@@ -210,6 +215,7 @@ watch(
label="Alamat" label="Alamat"
placeholder="Masukkan alamat" placeholder="Masukkan alamat"
is-required is-required
:is-disabled="isReadonly"
:col-span="2" :col-span="2"
/> />
<DE.Cell class="flex-row gap-2"> <DE.Cell class="flex-row gap-2">
@@ -220,6 +226,7 @@ watch(
placeholder="01" placeholder="01"
numeric-only numeric-only
:max-length="2" :max-length="2"
:is-disabled="isReadonly"
/> />
<InputBase <InputBase
field-name="rw" field-name="rw"
@@ -227,6 +234,7 @@ watch(
placeholder="02" placeholder="02"
:max-length="2" :max-length="2"
numeric-only numeric-only
:is-disabled="isReadonly"
/> />
</div> </div>
</DE.Cell> </DE.Cell>
@@ -14,9 +14,9 @@ interface FormData extends PersonContactFormData {}
interface Props { interface Props {
title: string title: string
isReadonly: boolean
contactLimit?: number contactLimit?: number
initialValues?: any initialValues?: any
isReadonly?: boolean
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -36,8 +36,6 @@ defineExpose({
setValues, setValues,
values, values,
}) })
const { title = 'Kontak Pasien', isReadonly = false } = props
</script> </script>
<template> <template>
@@ -94,7 +92,7 @@ const { title = 'Kontak Pasien', isReadonly = false } = props
preset="add" preset="add"
label="Tambah Kontak" label="Tambah Kontak"
title="Tambah Kontak ke Daftar Kontak" title="Tambah Kontak ke Daftar Kontak"
:disabled="fields.length >= contactLimit || isReadonly" :disabled="isReadonly || fields.length >= contactLimit"
:full-width-mobile="true" :full-width-mobile="true"
class="mt-4" class="mt-4"
@click="push({ contactType: '', contactNumber: '' })" @click="push({ contactType: '', contactNumber: '' })"
@@ -14,7 +14,7 @@ interface FormData extends PersonRelativeFormData {}
interface Props { interface Props {
title: string title: string
isReadonly?: boolean isReadonly: boolean
initialValues?: any initialValues?: any
contactLimit?: number contactLimit?: number
} }
@@ -36,7 +36,7 @@ defineExpose({
values: values, values: values,
}) })
const { title = 'Kontak Pasien', isReadonly = false, contactLimit = 5 } = props const { title = 'Kontak Pasien', contactLimit = 5 } = props
</script> </script>
<template> <template>
@@ -14,6 +14,7 @@ interface FormData extends PersonFamiliesFormData {}
interface Props { interface Props {
title: string title: string
isReadonly: boolean
initialValues?: any initialValues?: any
} }
@@ -84,7 +85,10 @@ watch(
<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">
<div class="mb-6"> <div class="mb-6">
<RadioParentsInput field-name="shareFamilyData" /> <RadioParentsInput
field-name="shareFamilyData"
:is-disabled="isReadonly"
/>
</div> </div>
<div <div
@@ -111,21 +115,21 @@ watch(
:field-name="`families[${idx}].name`" :field-name="`families[${idx}].name`"
label="Nama" label="Nama"
placeholder="Masukkan nama" placeholder="Masukkan nama"
:is-disabled="isFamilyFormDisabled" :is-disabled="isReadonly || isFamilyFormDisabled"
/> />
<SelectEducation <SelectEducation
:field-name="`families[${idx}].education`" :field-name="`families[${idx}].education`"
label="Pendidikan" label="Pendidikan"
placeholder="Pilih" placeholder="Pilih"
:is-disabled="isFamilyFormDisabled" :is-disabled="isReadonly || isFamilyFormDisabled"
/> />
<InputBase <InputBase
:field-name="`families[${idx}].occupation`" :field-name="`families[${idx}].occupation`"
label="Pekerjaan" label="Pekerjaan"
placeholder="Masukkan pekerjaan" placeholder="Masukkan pekerjaan"
:is-disabled="isFamilyFormDisabled" :is-disabled="isReadonly || isFamilyFormDisabled"
/> />
</DE.Block> </DE.Block>
</div> </div>
@@ -16,6 +16,7 @@ const props = defineProps<{
radioItemClass?: string radioItemClass?: string
labelClass?: string labelClass?: string
isRequired?: boolean isRequired?: boolean
isDisabled?: boolean
}>() }>()
const { const {
@@ -55,6 +56,7 @@ const residenceOptions = [
<FormControl> <FormControl>
<RadioGroup <RadioGroup
v-bind="componentField" v-bind="componentField"
:disabled="isDisabled"
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)" :class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
> >
<div <div
@@ -63,20 +65,22 @@ const residenceOptions = [
:class="cn('flex min-w-fit items-center space-x-2', radioItemClass)" :class="cn('flex min-w-fit items-center space-x-2', radioItemClass)"
> >
<RadioGroupItem <RadioGroupItem
:disabled="isDisabled"
:id="`${fieldName}-${index}`" :id="`${fieldName}-${index}`"
:value="option.value" :value="option.value"
:class=" :class="
cn( cn(
'relative h-4 w-4 rounded-full border-2 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5', 'peer relative h-4 w-4 rounded-full border-2 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
containerClass, containerClass,
) )
" "
/> />
<RadioLabel <RadioLabel
:for="`${fieldName}-${index}`" :for="isDisabled ? undefined : `${fieldName}-${index}`"
:class=" :class="
cn( cn(
'cursor-pointer select-none text-xs font-medium leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm', 'select-none text-xs font-medium leading-none transition-colors sm:text-sm',
isDisabled ? 'cursor-not-allowed opacity-70' : 'cursor-pointer hover:text-primary',
labelClass, labelClass,
) )
" "
+251 -137
View File
@@ -1,20 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
// type // type
import { withBase } from '~/models/_base' import { withBase } from '~/models/_base'
import type { PatientEntity, PatientBase, Patient, genPatientProps } from '~/models/patient'
import type { Person } from '~/models/person' 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 { ExposedForm } from '~/types/form'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types' import type { PatientEntity, PatientBase, Patient, genPatientProps } from '~/models/patient'
import { genPatientEntity } 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 // components
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue' 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 { getPatientDetail, uploadAttachment } from '~/services/patient.service'
import { import {
// for form entry
isReadonly, isReadonly,
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
@@ -41,6 +33,14 @@ import {
import { toast } from '~/components/pub/ui/toast' 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 // #region Props & Emits
const props = defineProps<{ const props = defineProps<{
callbackUrl?: string callbackUrl?: string
@@ -70,51 +70,178 @@ const patient = ref(
personRelatives: [], 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 // #endregion
// #region Lifecycle Hooks // #region Lifecycle Hooks
onMounted(() => { onMounted(async () => {
// 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,
)
}
}
})
// if edit mode, fetch patient detail // if edit mode, fetch patient detail
if (props.mode === 'edit' && props.patientId) { if (props.mode === 'edit' && props.patientId) {
void getPatientDetail(props.patientId as number).then((v) => { await loadInitData(props.patientId)
if (v.success) {
patient.value = v.body.data || {}
}
})
} }
}) })
// #endregion // #endregion
// #region Functions // #region Functions
async function composeFormData(): Promise<Patient> { async function composeFormData(): Promise<PatientEntity> {
const [patient, address, addressRelative, families, contacts, emergencyContact] = await Promise.all([ const [patient, address, addressRelative, families, contacts, emergencyContact] = await Promise.all([
personPatientForm.value?.validate(), personPatientForm.value?.validate(),
personAddressForm.value?.validate(), personAddressForm.value?.validate(),
@@ -141,7 +268,7 @@ async function composeFormData(): Promise<Patient> {
responsible: emergencyContact?.values, responsible: emergencyContact?.values,
} }
const formData = genPatient() const formData = genPatientEntity(formDataRequest)
if (patient?.values.residentIdentityFile) { if (patient?.values.residentIdentityFile) {
residentIdentityFile.value = patient?.values.residentIdentityFile residentIdentityFile.value = patient?.values.residentIdentityFile
@@ -156,6 +283,20 @@ async function composeFormData(): Promise<Patient> {
// #endregion region // #endregion region
// #region Utilities & event handlers // #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) { async function handleActionClick(eventType: string) {
try { try {
if (eventType === 'submit') { if (eventType === 'submit') {
@@ -197,10 +338,12 @@ async function handleActionClick(eventType: string) {
return return
} }
// handleCancelForm()
}
if (eventType === 'back') {
await navigateTo({ await navigateTo({
name: 'client-patient', name: 'client-patient',
}) })
// handleCancelForm()
} }
} catch (error) { } catch (error) {
// Show error toast to user // Show error toast to user
@@ -228,124 +371,95 @@ async function handleActionClick(eventType: string) {
// #endregion // #endregion
// #region Watchers // #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( watch(
[() => personAddressForm.value, () => personAddressRelativeForm.value], [() => personAddressForm.value?.values, () => personAddressRelativeForm.value?.values?.isSameAddress],
([addressForm, relativeForm]) => { () => {
if (addressForm && relativeForm) { if (isSameAddressActive()) {
// Trigger initial sync jika isSameAddress adalah true syncAddressToRelative()
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,
)
} }
}, },
{ deep: true, immediate: true },
) )
// #endregion // #endregion
</script> </script>
<template> <template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Tambah Pasien</div> <div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">
<AppPatientEntryForm ref="personPatientForm" /> {{ 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> <div class="h-6"></div>
<AppPersonAddressEntryForm <AppPersonAddressEntryForm
:key="`address-${formKey}`"
ref="personAddressForm" ref="personAddressForm"
title="Alamat Sekarang" title="Alamat Sekarang"
:is-readonly="isProcessing || isReadonly"
:initial-values="addressFormInitialValues"
/> />
<div class="h-6"></div> <div class="h-6"></div>
<AppPersonAddressEntryFormRelative <AppPersonAddressEntryFormRelative
:key="`address-rel-${formKey}`"
ref="personAddressRelativeForm" ref="personAddressRelativeForm"
title="Alamat KTP" title="Alamat KTP"
:is-readonly="isProcessing || isReadonly"
:initial-values="addressRelativeFormInitialValues"
/> />
<div class="h-6"></div> <div class="h-6"></div>
<AppPersonFamilyParentsForm <AppPersonFamilyParentsForm
:key="`family-${formKey}`"
ref="personFamilyForm" ref="personFamilyForm"
title="Identitas Orang Tua" title="Identitas Orang Tua"
:is-readonly="isProcessing || isReadonly"
:initial-values="familyFormInitialValues"
/> />
<div class="h-6"></div> <div class="h-6"></div>
<AppPersonContactEntryForm <AppPersonContactEntryForm
:key="`contact-${formKey}`"
ref="personContactForm" ref="personContactForm"
title="Kontak Pasien" title="Kontak Pasien"
:is-readonly="isProcessing || isReadonly"
:initial-values="contactFormInitialValues"
/> />
<AppPersonRelativeEntryForm <AppPersonRelativeEntryForm
:key="`responsible-${formKey}`"
ref="personEmergencyContactRelative" ref="personEmergencyContactRelative"
title="Penanggung Jawab" title="Penanggung Jawab"
:is-readonly="isProcessing || isReadonly"
:initial-values="responsibleFormInitialValues"
/> />
<div class="my-2 flex justify-end py-2"> <div class="my-2 flex justify-end py-2">