refactor(address): update address models and forms to use standardized fields

- Add preload relationships to address-related models
- Rename postalCode to postalCode_code for consistency
- Simplify location type handling with hidden fields
- Update validation schemas and form components
- Improve address display in patient preview
This commit is contained in:
Khafid Prayoga
2025-10-10 15:36:54 +07:00
parent a5d5e8acd1
commit ea04f33ad1
14 changed files with 122 additions and 109 deletions

No files matched your search

+12 -14
View File
@@ -1,13 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
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 DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue' import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
import DetailSection from '~/components/pub/my-ui/form/view/detail-section.vue' import DetailSection from '~/components/pub/my-ui/form/view/detail-section.vue'
import { type PersonAddress, formatAddress } from '~/models/person-address'
import { import {
addressLocationTypeCode,
educationCodes, educationCodes,
genderCodes, genderCodes,
occupationCodes, occupationCodes,
@@ -37,16 +35,16 @@ const relationshipOptions = mapToComboboxOptList(relationshipCodes)
const personContactTypeOptions = mapToComboboxOptList(personContactTypes) const personContactTypeOptions = mapToComboboxOptList(personContactTypes)
// Computed addresses from nested data // Computed addresses from nested data
const residentAddress = computed(() => { const domicileAddress = computed(() => {
const addresses = props.person.addresses const addresses = props.person.addresses
const resident = addresses?.find((addr) => addr.locationType === 'resident') const resident = addresses?.find((addr) => addr.locationType === 'domicile')
return resident?.address || 'Jl. Puncak Borobudur Blok M No. 321, Lowokwaru, Kota Malang, Jawa Timur' return formatAddress(resident)
}) })
const primaryAddress = computed(() => { const identityAddress = computed(() => {
const addresses = props.person.addresses const addresses = props.person.addresses
const primary = addresses?.find((addr) => addr.locationType === 'primary') const primary = addresses?.find((addr) => addr.locationType === 'identity')
return primary?.address || 'Perumahan Araya Cluster B, No 22, Blimbing, Kota Malang, Jawa Timur' return formatAddress(primary)
}) })
const patientAge = computed(() => { const patientAge = computed(() => {
@@ -68,6 +66,7 @@ const patientAge = computed(() => {
// #endregion // #endregion
// #region Functions // #region Functions
// #endregion region // #endregion region
// #region Utilities & event handlers // #region Utilities & event handlers
@@ -83,10 +82,9 @@ function onClick(type: string) {
<template> <template>
<DetailSection title="Data Pasien"> <DetailSection title="Data Pasien">
<DetailRow label="Nomor ID">{{ person.id || '-' }}</DetailRow> <DetailRow label="Nomor ID">{{ person.id || '-' }}</DetailRow>
<DetailRow label="Sapaan">{{ '-' }}</DetailRow>
<DetailRow label="Nama Lengkap">{{ person.name || '-' }}</DetailRow> <DetailRow label="Nama Lengkap">{{ person.name || '-' }}</DetailRow>
<DetailRow label="Tempat, tanggal lahir"> <DetailRow label="Tempat, tanggal lahir">
{{ person.birthRegency_code || '-' }}, {{ person.birthRegency?.name || '-' }},
{{ person.birthDate ? new Date(person.birthDate).toLocaleDateString() : '-' }} {{ person.birthDate ? new Date(person.birthDate).toLocaleDateString() : '-' }}
</DetailRow> </DetailRow>
<DetailRow label="Usia">{{ patientAge || '-' }}</DetailRow> <DetailRow label="Usia">{{ patientAge || '-' }}</DetailRow>
@@ -117,8 +115,8 @@ function onClick(type: string) {
</DetailSection> </DetailSection>
<DetailSection title="Alamat"> <DetailSection title="Alamat">
<DetailRow label="Alamat Domisili">{{ residentAddress || '-' }}</DetailRow> <DetailRow :label="addressLocationTypeCode.domicile || 'Alamat Domisili'">{{ domicileAddress || '-' }}</DetailRow>
<DetailRow label="Alamat KTP">{{ primaryAddress || '-' }}</DetailRow> <DetailRow :label="addressLocationTypeCode.identity || 'Alamat KTP'">{{ identityAddress || '-' }}</DetailRow>
</DetailSection> </DetailSection>
<DetailSection title="Kontak"> <DetailSection title="Kontak">
<template v-if="person.contacts && person.contacts.length > 0"> <template v-if="person.contacts && person.contacts.length > 0">
@@ -18,14 +18,7 @@ const props = defineProps<{
isRequired?: boolean isRequired?: boolean
}>() }>()
const { const { fieldName = 'zipCode', placeholder = 'Kode Pos', errors, class: containerClass, fieldGroupClass } = props
fieldName = 'zipCode',
placeholder = 'Kode Pos',
errors,
class: containerClass,
selectClass,
fieldGroupClass,
} = props
const villageCodeRef = toRef(props, 'villageCode') const villageCodeRef = toRef(props, 'villageCode')
const { postalCodeOptions, isLoading, error } = usePostalCodes(villageCodeRef) const { postalCodeOptions, isLoading, error } = usePostalCodes(villageCodeRef)
@@ -43,7 +43,7 @@ const fieldStates: Record<string, { dependsOn?: string; placeholder: string }> =
regency_code: { dependsOn: 'province_code', placeholder: 'Pilih provinsi dahulu' }, regency_code: { dependsOn: 'province_code', placeholder: 'Pilih provinsi dahulu' },
district_code: { dependsOn: 'regency_code', placeholder: 'Pilih kabupaten/kota dahulu' }, district_code: { dependsOn: 'regency_code', placeholder: 'Pilih kabupaten/kota dahulu' },
village_code: { dependsOn: 'district_code', placeholder: 'Pilih kecamatan dahulu' }, village_code: { dependsOn: 'district_code', placeholder: 'Pilih kecamatan dahulu' },
postalCode: { dependsOn: 'village_code', placeholder: 'Pilih kelurahan dahulu' }, postalCode_code: { dependsOn: 'village_code', placeholder: 'Pilih kelurahan dahulu' },
address: { placeholder: 'Masukkan alamat' }, address: { placeholder: 'Masukkan alamat' },
rt: { placeholder: '001' }, rt: { placeholder: '001' },
rw: { placeholder: '002' }, rw: { placeholder: '002' },
@@ -73,7 +73,7 @@ function getFieldState(field: string) {
const isDisabledByDependency = !dependencyValue const isDisabledByDependency = !dependencyValue
// Jika isSame, semua field location disabled // Jika isSame, semua field location disabled
if (isSame && ['regency_code', 'district_code', 'village_code', 'postalCode'].includes(field)) { if (isSame && ['regency_code', 'district_code', 'village_code', 'postalCode_code'].includes(field)) {
return { placeholder: '-', disabled: true } return { placeholder: '-', disabled: true }
} }
@@ -83,7 +83,7 @@ function getFieldState(field: string) {
} }
// Jika isSame dan field location, disabled // Jika isSame dan field location, disabled
if (isSame && ['regency_code', 'district_code', 'village_code', 'postalCode'].includes(field)) { if (isSame && ['regency_code', 'district_code', 'village_code', 'postalCode_code'].includes(field)) {
return { placeholder: '-', disabled: true } return { placeholder: '-', disabled: true }
} }
@@ -108,7 +108,7 @@ watch(
regency_code: undefined, regency_code: undefined,
district_code: undefined, district_code: undefined,
village_code: undefined, village_code: undefined,
postalCode: undefined, postalCode_code: undefined,
}, },
false, false,
) )
@@ -133,7 +133,7 @@ watch(
{ {
district_code: undefined, district_code: undefined,
village_code: undefined, village_code: undefined,
postalCode: undefined, postalCode_code: undefined,
}, },
false, false,
) )
@@ -157,7 +157,7 @@ watch(
formRef.value.setValues( formRef.value.setValues(
{ {
village_code: undefined, village_code: undefined,
postalCode: undefined, postalCode_code: undefined,
}, },
false, false,
) )
@@ -180,7 +180,7 @@ watch(
formRef.value.setValues( formRef.value.setValues(
{ {
postalCode: undefined, postalCode_code: undefined,
}, },
false, false,
) )
@@ -214,7 +214,7 @@ watch(
if (updatedValues.regency_code === '') updatedValues.regency_code = undefined if (updatedValues.regency_code === '') updatedValues.regency_code = undefined
if (updatedValues.district_code === '') updatedValues.district_code = undefined if (updatedValues.district_code === '') updatedValues.district_code = undefined
if (updatedValues.village_code === '') updatedValues.village_code = undefined if (updatedValues.village_code === '') updatedValues.village_code = undefined
if (updatedValues.postalCode === '') updatedValues.postalCode = undefined if (updatedValues.postalCode_code === '') updatedValues.postalCode_code = undefined
if (updatedValues.address === '') updatedValues.address = undefined if (updatedValues.address === '') updatedValues.address = undefined
// Update values dan trigger validasi // Update values dan trigger validasi
@@ -235,7 +235,7 @@ watch(
formRef.value?.setFieldError('regency_code', undefined) formRef.value?.setFieldError('regency_code', undefined)
formRef.value?.setFieldError('district_code', undefined) formRef.value?.setFieldError('district_code', undefined)
formRef.value?.setFieldError('village_code', undefined) formRef.value?.setFieldError('village_code', undefined)
formRef.value?.setFieldError('postalCode', undefined) formRef.value?.setFieldError('postalCode_code', undefined)
formRef.value?.setFieldError('address', undefined) formRef.value?.setFieldError('address', undefined)
formRef.value?.setFieldError('rt', undefined) formRef.value?.setFieldError('rt', undefined)
formRef.value?.setFieldError('rw', undefined) formRef.value?.setFieldError('rw', undefined)
@@ -255,7 +255,7 @@ watch(
:validation-schema="formSchema" :validation-schema="formSchema"
:validate-on-mount="false" :validate-on-mount="false"
validation-mode="onSubmit" validation-mode="onSubmit"
:initial-values="initialValues ?? { isSameAddress: '1' }" :initial-values="initialValues ?? { isSameAddress: '1', locationType: 'identity' }"
> >
<div> <div>
<p <p
@@ -267,34 +267,17 @@ watch(
</div> </div>
<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="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2">
<!-- LocationType --> <!-- LocationType - Hidden field with default value 'identity' -->
<FieldGroup v-if="conf?.withAddressName"> <FormField
<Label label-for="locationType">Jenis Alamat</Label> v-slot="{ componentField }"
<Field name="locationType"
id="locationType" >
:errors="errors" <input
> type="hidden"
<FormField v-bind="componentField"
v-slot="{ componentField }" value="primary"
name="locationType" />
> </FormField>
<FormItem>
<FormControl>
<Select
id="locationType"
v-bind="componentField"
:items="[
{ label: 'Rumah', value: 'rumah' },
{ label: 'Kantor', value: 'kantor' },
{ label: 'Lainnya', value: 'lainnya' },
]"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup class="radio-group-field"> <FieldGroup class="radio-group-field">
<Label <Label
size="fit" size="fit"
@@ -420,10 +403,10 @@ watch(
<div class="min-w-0 flex-[2]"> <div class="min-w-0 flex-[2]">
<SelectPostal <SelectPostal
field-name="postalCode" field-name="postalCode_code"
:village-code="values.village_code" :village-code="values.village_code"
:placeholder="getFieldState('postalCode').placeholder" :placeholder="getFieldState('postalCode_code').placeholder"
:is-disabled="getFieldState('postalCode').disabled || !values.village_code" :is-disabled="getFieldState('postalCode_code').disabled || !values.village_code"
/> />
</div> </div>
</div> </div>
@@ -160,7 +160,7 @@ watch(
:validation-schema="formSchema" :validation-schema="formSchema"
:validate-on-mount="false" :validate-on-mount="false"
validation-mode="onSubmit" validation-mode="onSubmit"
:initial-values="initialValues ? initialValues : {}" :initial-values="initialValues ? { locationType: 'domicile', ...initialValues } : { locationType: 'domicile' }"
> >
<div> <div>
<p <p
@@ -172,34 +172,17 @@ watch(
</div> </div>
<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="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2">
<!-- LocationType --> <!-- LocationType - Hidden field with default value 'domicile' -->
<FieldGroup v-if="conf?.withAddressName"> <FormField
<Label label-for="locationType">Jenis Alamat</Label> v-slot="{ componentField }"
<Field name="locationType"
id="locationType" >
:errors="errors" <input
> type="hidden"
<FormField v-bind="componentField"
v-slot="{ componentField }" value="resident"
name="locationType" />
> </FormField>
<FormItem>
<FormControl>
<Select
id="locationType"
v-bind="componentField"
:items="[
{ label: 'Rumah', value: 'rumah' },
{ label: 'Kantor', value: 'kantor' },
{ label: 'Lainnya', value: 'lainnya' },
]"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<div class="flex-row gap-2 md:flex"> <div class="flex-row gap-2 md:flex">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
@@ -268,7 +251,7 @@ watch(
<div class="min-w-0 flex-[2]"> <div class="min-w-0 flex-[2]">
<SelectPostal <SelectPostal
field-name="postalCode" field-name="postalCode_code"
placeholder="Pilih kelurahan dahulu" placeholder="Pilih kelurahan dahulu"
:village-code="values.village_code" :village-code="values.village_code"
:is-disabled="!values.village_code" :is-disabled="!values.village_code"
+4 -4
View File
@@ -50,7 +50,7 @@ onMounted(() => {
regency_code: currentAddressValues.regency_code || undefined, regency_code: currentAddressValues.regency_code || undefined,
district_code: currentAddressValues.district_code || undefined, district_code: currentAddressValues.district_code || undefined,
village_code: currentAddressValues.village_code || undefined, village_code: currentAddressValues.village_code || undefined,
postalCode: currentAddressValues.postalCode || undefined, postalCode_code: currentAddressValues.postalCode_code || undefined,
address: currentAddressValues.address || undefined, address: currentAddressValues.address || undefined,
rt: currentAddressValues.rt || undefined, rt: currentAddressValues.rt || undefined,
rw: currentAddressValues.rw || undefined, rw: currentAddressValues.rw || undefined,
@@ -141,7 +141,7 @@ watch(
regency_code: currentAddressValues.regency_code || undefined, regency_code: currentAddressValues.regency_code || undefined,
district_code: currentAddressValues.district_code || undefined, district_code: currentAddressValues.district_code || undefined,
village_code: currentAddressValues.village_code || undefined, village_code: currentAddressValues.village_code || undefined,
postalCode: currentAddressValues.postalCode || undefined, postalCode_code: currentAddressValues.postalCode_code || undefined,
address: currentAddressValues.address || undefined, address: currentAddressValues.address || undefined,
rt: currentAddressValues.rt || undefined, rt: currentAddressValues.rt || undefined,
rw: currentAddressValues.rw || undefined, rw: currentAddressValues.rw || undefined,
@@ -172,7 +172,7 @@ watch(
regency_code: newAddressValues.regency_code || undefined, regency_code: newAddressValues.regency_code || undefined,
district_code: newAddressValues.district_code || undefined, district_code: newAddressValues.district_code || undefined,
village_code: newAddressValues.village_code || undefined, village_code: newAddressValues.village_code || undefined,
postalCode: newAddressValues.postalCode || undefined, postalCode_code: newAddressValues.postalCode_code || undefined,
address: newAddressValues.address || undefined, address: newAddressValues.address || undefined,
rt: newAddressValues.rt || undefined, rt: newAddressValues.rt || undefined,
rw: newAddressValues.rw || undefined, rw: newAddressValues.rw || undefined,
@@ -202,7 +202,7 @@ watch(
regency_code: currentAddressValues.regency_code || undefined, regency_code: currentAddressValues.regency_code || undefined,
district_code: currentAddressValues.district_code || undefined, district_code: currentAddressValues.district_code || undefined,
village_code: currentAddressValues.village_code || undefined, village_code: currentAddressValues.village_code || undefined,
postalCode: currentAddressValues.postalCode || undefined, postalCode_code: currentAddressValues.postalCode_code || undefined,
address: currentAddressValues.address || undefined, address: currentAddressValues.address || undefined,
rt: currentAddressValues.rt || undefined, rt: currentAddressValues.rt || undefined,
rw: currentAddressValues.rw || undefined, rw: currentAddressValues.rw || undefined,
+13 -5
View File
@@ -334,10 +334,18 @@ export const infraGroupCodes: Record<string, string> = {
warehouse: 'Gudang / Depo', warehouse: 'Gudang / Depo',
room: 'Ruang', room: 'Ruang',
chamber: 'Kamar', chamber: 'Kamar',
bed: 'Ranjang' bed: 'Ranjang',
} }
export const infraGroupCodesKeys: Record<string, string> = Object.keys(infraGroupCodes).reduce((acc, key) => { export const infraGroupCodesKeys: Record<string, string> = Object.keys(infraGroupCodes).reduce(
acc[key] = key (acc, key) => {
return acc acc[key] = key
}, {} as Record<string, string>) return acc
},
{} as Record<string, string>,
)
export const addressLocationTypeCode: Record<string, string> = {
identity: 'Alamat KTP',
domicile: 'Alamat Domisili',
}
+4
View File
@@ -1,9 +1,13 @@
import { type Base, genBase } from './_base' import { type Base, genBase } from './_base'
import type { Regency } from './regency'
export interface District extends Base { export interface District extends Base {
regency_code: string regency_code: string
code: string code: string
name: string name: string
// preload
regency?: Regency | null
} }
export function genDistrict(): District { export function genDistrict(): District {
+26 -1
View File
@@ -1,4 +1,6 @@
import { type Base, genBase } from './_base' import { type Base, genBase } from './_base'
import type { PostalCode } from './postal-code'
import { toTitleCase } from '~/lib/utils'
export interface PersonAddress extends Base { export interface PersonAddress extends Base {
person_id: number person_id: number
@@ -6,8 +8,11 @@ export interface PersonAddress extends Base {
address: string address: string
rt?: string rt?: string
rw?: string rw?: string
postalCode?: string postalCode_code?: string
village_code: string village_code: string
// preload
postalCode?: PostalCode | null
} }
export function genPersonAddress(): PersonAddress { export function genPersonAddress(): PersonAddress {
@@ -19,3 +24,23 @@ export function genPersonAddress(): PersonAddress {
village_code: '', village_code: '',
} }
} }
export function formatAddress(builder?: PersonAddress) {
if (!builder) return ''
const village = builder?.postalCode?.village
const district = village?.district
const regency = district?.regency
const province = regency?.province
const parts = [
builder?.address,
village?.name,
district?.name,
toTitleCase(regency?.name || ''),
toTitleCase(province?.name || ''),
builder?.postalCode_code,
].filter(Boolean)
return parts.join(', ')
}
+6
View File
@@ -1,7 +1,10 @@
import { type Base, genBase } from './_base' import { type Base, genBase } from './_base'
import type { Ethnic } from './ethnic'
import type { Language } from './language'
import type { PersonAddress } from './person-address' import type { PersonAddress } from './person-address'
import type { PersonContact } from './person-contact' import type { PersonContact } from './person-contact'
import type { PersonRelative } from './person-relative' import type { PersonRelative } from './person-relative'
import type { Regency } from './regency'
export interface Person extends Base { export interface Person extends Base {
name: string name: string
@@ -29,9 +32,12 @@ export interface Person extends Base {
familyIdentityFileUrl?: string familyIdentityFileUrl?: string
// preload data for detail patient // preload data for detail patient
birthRegency?: Regency | null
addresses?: PersonAddress[] | null addresses?: PersonAddress[] | null
contacts?: PersonContact[] | null contacts?: PersonContact[] | null
relatives?: PersonRelative[] | null relatives?: PersonRelative[] | null
ethnic?: Ethnic | null
language?: Language | null
} }
export function genPerson(): Person { export function genPerson(): Person {
+4 -1
View File
@@ -1,8 +1,11 @@
import { type Base, genBase } from './_base' import { type Base, genBase } from './_base'
import type { Village } from './village'
export interface PostalCode extends Base { export interface PostalCode extends Base {
code: string code: string
village_code: string village_code: string
// preload
village?: Village | null
} }
export function genPostalCode(): PostalCode { export function genPostalCode(): PostalCode {
+4 -1
View File
@@ -1,9 +1,12 @@
import { type Base, genBase } from "./_base" import { type Base, genBase } from './_base'
import type { Province } from './province'
export interface Regency extends Base { export interface Regency extends Base {
code: string code: string
name: string name: string
province_code: string province_code: string
province?: Province | null
} }
export function genRegency(): Regency { export function genRegency(): Regency {
+4
View File
@@ -1,9 +1,13 @@
import { type Base, genBase } from './_base' import { type Base, genBase } from './_base'
import type { District } from './district'
export interface Village extends Base { export interface Village extends Base {
district_code: string district_code: string
code: string code: string
name: string name: string
// preload
district?: District | null
} }
export function genVillage(): Village { export function genVillage(): Village {
@@ -22,8 +22,8 @@ const PersonAddressRelativeSchema = z
'regency_code', 'regency_code',
'district_code', 'district_code',
'village_code', 'village_code',
'postalCode', 'postalCode_code',
'address' 'address',
] ]
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
@@ -48,7 +48,7 @@ function getRequiredMessage(field: string): string {
regency_code: 'Mohon pilih kabupaten/kota', regency_code: 'Mohon pilih kabupaten/kota',
district_code: 'Mohon pilih kecamatan', district_code: 'Mohon pilih kecamatan',
village_code: 'Mohon pilih kelurahan', village_code: 'Mohon pilih kelurahan',
postalCode: 'Mohon lengkapi kode pos', postalCode_code: 'Mohon lengkapi kode pos',
address: 'Mohon lengkapi alamat', address: 'Mohon lengkapi alamat',
} }
return messages[field] || `${field} wajib diisi` return messages[field] || `${field} wajib diisi`
+4 -1
View File
@@ -1,6 +1,9 @@
import { z } from 'zod' import { z } from 'zod'
const PersonAddressSchema = z.object({ const PersonAddressSchema = z.object({
locationType: z.string({
required_error: 'Mohon pilih jenis alamat',
}),
address: z.string({ address: z.string({
required_error: 'Mohon lengkapi alamat', required_error: 'Mohon lengkapi alamat',
}), }),
@@ -16,7 +19,7 @@ const PersonAddressSchema = z.object({
village_code: z.string({ village_code: z.string({
required_error: 'Mohon pilih kelurahan', required_error: 'Mohon pilih kelurahan',
}), }),
postalCode: z.string({ postalCode_code: z.string({
required_error: 'Mohon lengkapi kode pos', required_error: 'Mohon lengkapi kode pos',
}), }),
// .min(5, 'Kode pos harus berupa angka 5 digit') // .min(5, 'Kode pos harus berupa angka 5 digit')