feat(patient): enhance patient detail view with accordion and timezone support

- Add date-fns-tz for timezone-aware date formatting
- Refactor patient detail view to use accordion components
- Improve date display formatting with locale support
- Update navigation handling for edit and back actions
- Extend ClickType enum to include 'edit' action
This commit is contained in:
Khafid Prayoga
2025-12-05 19:24:40 +07:00
parent d848e5bd07
commit 41985ea89f
5 changed files with 241 additions and 140 deletions
+199 -116
View File
@@ -1,9 +1,14 @@
<script setup lang="ts">
import type { Patient } from '~/models/patient'
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 { formatAddress } from '~/models/person-address'
import { format } from 'date-fns'
import { formatInTimeZone } from 'date-fns-tz'
import { id } from 'date-fns/locale'
// types
import type { Patient } from '~/models/patient'
import type { ClickType } from '~/components/pub/my-ui/nav-footer'
// helper
import { formatAddress } from '~/models/person-address'
import {
addressLocationTypeCode,
educationCodes,
@@ -15,13 +20,21 @@ import {
} from '~/lib/constants'
import { mapToComboboxOptList } from '~/lib/utils'
// components
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '~/components/pub/ui/accordion'
import { Fragment } from '~/components/pub/my-ui/form/'
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 { toZoned } from '@internationalized/date'
// #region Props & Emits
const props = defineProps<{
patient: Patient
}>()
const emit = defineEmits<{
(e: 'click', type: string): void
(e: 'back'): void
(e: 'edit'): void
}>()
// #endregion
@@ -70,8 +83,9 @@ const patientAge = computed(() => {
// #endregion region
// #region Utilities & event handlers
function onClick(type: string) {
emit('click', type)
function onNavigate(type: ClickType) {
if (type == 'back') emit('back')
if (type == 'edit') emit('edit')
}
// #endregion
@@ -80,120 +94,189 @@ function onClick(type: string) {
</script>
<template>
<DetailSection title="Data Pasien">
<DetailRow label="Nomor">{{ patient.number || '-' }}</DetailRow>
<DetailRow label="Nama Lengkap">{{ patient.person.name || '-' }}</DetailRow>
<DetailRow label="Tempat, tanggal lahir">
{{ patient.person.birthRegency?.name || '-' }},
{{ patient.person.birthDate ? new Date(patient.person.birthDate).toLocaleDateString('id-ID') : '-' }}
</DetailRow>
<DetailRow label="Usia">{{ patientAge || '-' }} Tahun</DetailRow>
<DetailRow label="Tanggal Daftar">
{{ patient.person.createdAt ? new Date(patient.person.createdAt).toLocaleDateString('id-ID') : '-' }}
</DetailRow>
<DetailRow label="Jenis Kelamin">
{{ genderOptions.find((item) => item.code === patient.person.gender_code)?.label || '-' }}
</DetailRow>
<Accordion
type="multiple"
class="w-full"
collapsible
:defaultValue="['item-patient', 'item-address', 'item-contact', 'item-parents', 'item-relative']"
>
<Fragment
v-slot="{ section }"
title="Data Pasien"
>
<AccordionItem value="item-patient">
<AccordionTrigger>{{ section }}</AccordionTrigger>
<AccordionContent>
<DetailRow label="Nomor">{{ patient.number || '-' }}</DetailRow>
<DetailRow label="Nama Lengkap">{{ patient.person.name || '-' }}</DetailRow>
<DetailRow label="Tempat, tanggal lahir">
{{ patient.person.birthRegency?.name || '-' }},
{{
patient.person.birthDate
? format(new Date(patient.person.birthDate), 'dd MMMM yyyy', { locale: id })
: '-'
}}
</DetailRow>
<DetailRow label="Usia">{{ patientAge || '-' }} Tahun</DetailRow>
<DetailRow label="Tanggal Daftar">
{{
patient.person.createdAt
? formatInTimeZone(new Date(patient.person.createdAt), 'Asia/Jakarta', "dd MMMM yyyy, HH:mm:ss 'WIB'", {
locale: id,
})
: '-'
}}
</DetailRow>
<DetailRow label="Jenis Kelamin">
{{ genderOptions.find((item) => item.code === patient.person.gender_code)?.label || '-' }}
</DetailRow>
<DetailRow label="NIK">{{ patient.person.residentIdentityNumber || '-' }}</DetailRow>
<DetailRow label="No. SIM">{{ patient.person.drivingLicenseNumber || '-' }}</DetailRow>
<DetailRow label="No. Paspor">{{ patient.person.passportNumber || '-' }}</DetailRow>
<DetailRow label="NIK">{{ patient.person.residentIdentityNumber || '-' }}</DetailRow>
<DetailRow label="No. SIM">{{ patient.person.drivingLicenseNumber || '-' }}</DetailRow>
<DetailRow label="No. Paspor">{{ patient.person.passportNumber || '-' }}</DetailRow>
<DetailRow label="Agama">
{{ religionOptions.find((item) => item.code === patient.person.religion_code)?.label || '-' }}
</DetailRow>
<DetailRow label="Suku">{{ patient.person.ethnic?.name || '-' }}</DetailRow>
<DetailRow label="Bahasa">{{ patient.person.language?.name || '-' }}</DetailRow>
<DetailRow label="Pendidikan">
{{ educationOptions.find((item) => item.code === patient.person.education_code)?.label || '-' }}
</DetailRow>
<DetailRow label="Pekerjaan">
{{
occupationOptions.find((item) => item.code === patient.person.occupation_code)?.label ||
patient.person.occupation_name ||
'-'
}}
</DetailRow>
</DetailSection>
<DetailRow label="Agama">
{{ religionOptions.find((item) => item.code === patient.person.religion_code)?.label || '-' }}
</DetailRow>
<DetailRow label="Suku">{{ patient.person.ethnic?.name || '-' }}</DetailRow>
<DetailRow label="Bahasa">{{ patient.person.language?.name || '-' }}</DetailRow>
<DetailRow label="Pendidikan">
{{ educationOptions.find((item) => item.code === patient.person.education_code)?.label || '-' }}
</DetailRow>
<DetailRow label="Pekerjaan">
{{
occupationOptions.find((item) => item.code === patient.person.occupation_code)?.label ||
patient.person.occupation_name ||
'-'
}}
</DetailRow>
</AccordionContent>
</AccordionItem>
</Fragment>
<DetailSection title="Alamat">
<DetailRow :label="addressLocationTypeCode.domicile || 'Alamat Domisili'">{{ domicileAddress || '-' }}</DetailRow>
<DetailRow :label="addressLocationTypeCode.identity || 'Alamat KTP'">{{ identityAddress || '-' }}</DetailRow>
</DetailSection>
<DetailSection title="Kontak">
<template v-if="patient.person.contacts && patient.person.contacts.length > 0">
<template
v-for="contactType in personContactTypeOptions"
:key="contactType.code"
>
<DetailRow :label="contactType.label">
{{ patient.person.contacts.find((item) => item.type_code === contactType.code)?.value || '-' }}
</DetailRow>
</template>
</template>
<template v-else>
<DetailRow label="Kontak">-</DetailRow>
</template>
</DetailSection>
<DetailSection title="Orang Tua">
<template v-if="patient.person.relatives && patient.person.relatives.filter((rel) => !rel.responsible).length > 0">
<template
v-for="(relative, index) in patient.person.relatives.filter((rel) => !rel.responsible)"
:key="relative.id"
>
<div
v-if="index > 0"
class="mt-3 border-t border-gray-200 pt-3"
></div>
<DetailRow label="Nama">{{ relative.name || '-' }}</DetailRow>
<DetailRow label="Hubungan">
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
</DetailRow>
<!-- <DetailRow label="Jenis Kelamin">
<Fragment
v-slot="{ section }"
title="Alamat"
>
<AccordionItem value="item-address">
<AccordionTrigger>{{ section }}</AccordionTrigger>
<AccordionContent>
<DetailRow :label="addressLocationTypeCode.domicile || 'Alamat Domisili'">
{{ domicileAddress || '-' }}
</DetailRow>
<DetailRow :label="addressLocationTypeCode.identity || 'Alamat KTP'">
{{ identityAddress || '-' }}
</DetailRow>
</AccordionContent>
</AccordionItem>
</Fragment>
<Fragment
v-slot="{ section }"
title="Kontak"
>
<AccordionItem value="item-contact">
<AccordionTrigger>{{ section }}</AccordionTrigger>
<AccordionContent>
<template v-if="patient.person.contacts && patient.person.contacts.length > 0">
<template
v-for="contactType in personContactTypeOptions"
:key="contactType.code"
>
<DetailRow :label="contactType.label">
{{ patient.person.contacts.find((item) => item.type_code === contactType.code)?.value || '-' }}
</DetailRow>
</template>
</template>
<template v-else>
<DetailRow label="Kontak">-</DetailRow>
</template>
</AccordionContent>
</AccordionItem>
</Fragment>
<Fragment
v-slot="{ section }"
title="Data Orang Tua"
>
<AccordionItem value="item-parents">
<AccordionTrigger>{{ section }}</AccordionTrigger>
<AccordionContent>
<template
v-if="patient.person.relatives && patient.person.relatives.filter((rel) => !rel.responsible).length > 0"
>
<template
v-for="(relative, index) in patient.person.relatives.filter((rel) => !rel.responsible)"
:key="relative.id"
>
<div
v-if="index > 0"
class="mt-3 border-t border-gray-200 pt-3"
></div>
<DetailRow label="Nama">{{ relative.name || '-' }}</DetailRow>
<DetailRow label="Hubungan">
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
</DetailRow>
<!-- <DetailRow label="Jenis Kelamin">
{{ genderOptions.find((item) => item.code === relative.gender_code)?.label || '-' }}
</DetailRow> -->
<DetailRow label="Pendidikan">
{{ educationOptions.find((item) => item.code === relative.education_code)?.label || '-' }}
</DetailRow>
<DetailRow label="Pekerjaan">
{{
occupationOptions.find((item) => item.code === relative.occupation_code)?.label ||
relative.occupation_name ||
'-'
}}
</DetailRow>
<!-- <DetailRow label="Alamat">{{ relative.address || '-' }}</DetailRow> -->
<!-- <DetailRow label="Nomor HP">{{ relative.phoneNumber || '-' }}</DetailRow> -->
</template>
</template>
<template v-else>
<DetailRow label="Orang Tua">-</DetailRow>
</template>
</DetailSection>
<DetailSection title="Penanggung Jawab">
<template v-if="patient.person.relatives && patient.person.relatives.filter((rel) => rel.responsible).length > 0">
<template
v-for="(relative, index) in patient.person.relatives.filter((rel) => rel.responsible)"
:key="relative.id"
>
<div
v-if="index > 0"
class="mt-3 border-t border-gray-200 pt-3"
></div>
<DetailRow label="Nama">{{ relative.name || '-' }}</DetailRow>
<DetailRow label="Hubungan">
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
</DetailRow>
<DetailRow label="Alamat">{{ relative.address || '-' }}</DetailRow>
<DetailRow label="Nomor HP">{{ relative.phoneNumber || '-' }}</DetailRow>
</template>
</template>
<template v-else>
<DetailRow label="Penanggung Jawab">-</DetailRow>
</template>
</DetailSection>
<div class="border-t-1 my-2 flex justify-end border-t-slate-300 py-2">
<PubMyUiNavFooterBaEd @click="onClick" />
<DetailRow label="Pendidikan">
{{ educationOptions.find((item) => item.code === relative.education_code)?.label || '-' }}
</DetailRow>
<DetailRow label="Pekerjaan">
{{
occupationOptions.find((item) => item.code === relative.occupation_code)?.label ||
relative.occupation_name ||
'-'
}}
</DetailRow>
<!-- <DetailRow label="Alamat">{{ relative.address || '-' }}</DetailRow> -->
<!-- <DetailRow label="Nomor HP">{{ relative.phoneNumber || '-' }}</DetailRow> -->
</template>
</template>
<template v-else>
<DetailRow label="Orang Tua">-</DetailRow>
</template>
</AccordionContent>
</AccordionItem>
</Fragment>
<Fragment
v-slot="{ section }"
title="Data Penanggung Jawab"
>
<AccordionItem value="item-relative">
<AccordionTrigger>{{ section }}</AccordionTrigger>
<AccordionContent>
<template
v-if="patient.person.relatives && patient.person.relatives.filter((rel) => rel.responsible).length > 0"
>
<template
v-for="(relative, index) in patient.person.relatives.filter((rel) => rel.responsible)"
:key="relative.id"
>
<div
v-if="index > 0"
class="mt-3 border-t border-gray-200 pt-3"
></div>
<DetailRow label="Nama">{{ relative.name || '-' }}</DetailRow>
<DetailRow label="Hubungan">
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
</DetailRow>
<DetailRow label="Alamat">{{ relative.address || '-' }}</DetailRow>
<DetailRow label="Nomor HP">{{ relative.phoneNumber || '-' }}</DetailRow>
</template>
</template>
<template v-else>
<DetailRow label="Penanggung Jawab">-</DetailRow>
</template>
</AccordionContent>
</AccordionItem>
</Fragment>
</Accordion>
<div class="my-2 flex justify-end py-2">
<PubMyUiNavFooterBaEd @click="onNavigate" />
</div>
</template>
+29 -23
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { withBase } from '~/models/_base'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
import type { Patient } from '~/models/patient'
import type { PatientEntity } from '~/models/patient'
import type { Person } from '~/models/person'
// Components
@@ -18,7 +18,7 @@ const props = defineProps<{
// #region State & Computed
const patient = ref(
withBase<Patient>({
withBase<PatientEntity>({
person: {} as Person,
personAddresses: [],
personContacts: [],
@@ -47,19 +47,19 @@ onMounted(async () => {
// #endregion region
// #region Utilities & event handlers
function handleAction(type: string) {
switch (type) {
case 'edit':
// TODO: Handle edit action
console.log('editing data')
break
case 'cancel':
navigateTo({
name: 'client-patient',
})
break
}
async function onBack() {
await navigateTo({
name: 'client-patient',
})
}
async function onEdit() {
console.log(props.patientId)
await navigateTo({
name: 'client-patient-id-edit',
params: {
id: props.patientId,
},
})
}
// #endregion
@@ -68,13 +68,19 @@ function handleAction(type: string) {
</script>
<template>
<Header
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
/>
<div
v-if="patient"
:key="patient.id"
>
<Header
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
/>
<AppPatientPreview
:patient="patient"
@click="handleAction"
/>
<AppPatientPreview
:patient="patient"
@back="onBack"
@edit="onEdit"
/>
</div>
</template>
+1 -1
View File
@@ -1 +1 @@
export type ClickType = 'back' | 'draft' | 'submit'
export type ClickType = 'back' | 'draft' | 'submit' | 'edit'
+1
View File
@@ -21,6 +21,7 @@
"@unovis/ts": "^1.5.1",
"@unovis/vue": "^1.5.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"embla-carousel": "^8.5.2",
"embla-carousel-vue": "^8.5.2",
"file-saver": "^2.0.5",
+11
View File
@@ -26,6 +26,9 @@ dependencies:
date-fns:
specifier: ^4.1.0
version: 4.1.0
date-fns-tz:
specifier: ^3.2.0
version: 3.2.0(date-fns@4.1.0)
embla-carousel:
specifier: ^8.5.2
version: 8.6.0
@@ -5709,6 +5712,14 @@ packages:
d3-zoom: 3.0.0
dev: false
/date-fns-tz@3.2.0(date-fns@4.1.0):
resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==}
peerDependencies:
date-fns: ^3.0.0 || ^4.0.0
dependencies:
date-fns: 4.1.0
dev: false
/date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
dev: false