fix: improves encounter detail rendering and data mapping

This commit is contained in:
riefive
2025-11-14 15:02:49 +07:00
parent 0d4accc281
commit 840efeba8c
4 changed files with 240 additions and 105 deletions
@@ -138,91 +138,103 @@ const bedNumber = computed(() => {
<template> <template>
<div class="w-full rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950"> <div class="w-full rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
<!-- Data Pasien --> <!-- Data Pasien -->
<h2 class="mb-4 font-semibold text-base md:text-lg 2xl:text-xl"> <h2 class="mb-4 text-base font-semibold md:text-lg 2xl:text-xl">Data Pasien:</h2>
Data Pasien:
</h2>
<DE.Block <!-- 4 Column Grid Layout -->
mode="preview" <div class="grid grid-cols-1 gap-4 sm:grid-cols-4">
labelSize="large" <!-- No. RM -->
> <div class="flex gap-2">
<DE.Cell> <label class="w-[100px] flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">No. RM</label>
<DE.Label class="font-semibold">No. RM</DE.Label> <label class="w-[20px] flex-none">:</label>
<DE.Field> <p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ data.patient.number || '-' }} {{ data.patient.number || '-' }}
</DE.Field> </p>
</DE.Cell> </div>
<DE.Cell> <!-- Tanggal Lahir -->
<DE.Label class="font-semibold">Nama Pasien</DE.Label> <div class="flex gap-2">
<DE.Field> <label class="w-[100px] flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Tanggal Lahir</label>
{{ data.patient.person.name || '-' }} <label class="w-[20px] flex-none">:</label>
</DE.Field> <p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
</DE.Cell>
<DE.Cell>
<DE.Label class="font-semibold">Alamat</DE.Label>
<DE.Field>
{{ address }}
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label class="font-semibold">Tanggal Lahir</DE.Label>
<DE.Field>
{{ birthDateFormatted }} {{ birthDateFormatted }}
</DE.Field> </p>
</DE.Cell> </div>
<DE.Cell> <!-- Jenis Pembayaran -->
<DE.Label class="font-semibold">Tanggal Masuk RS</DE.Label> <div class="flex gap-2">
<DE.Field> <label class="w-[100px] flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">
{{ registeredDateFormatted }} Jenis Pembayaran
</DE.Field> </label>
</DE.Cell> <label class="w-[20px] flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
<DE.Cell>
<DE.Label class="font-semibold">Jenis Kelamin</DE.Label>
<DE.Field>
{{ genderLabel }}
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label class="font-semibold">Jenis Pembayaran</DE.Label>
<DE.Field>
{{ paymentTypeLabel }} {{ paymentTypeLabel }}
</DE.Field> </p>
</DE.Cell> </div>
<DE.Cell> <!-- No Bed -->
<DE.Label class="font-semibold">No. Billing</DE.Label> <div class="flex gap-2">
<DE.Field> <label class="w-[100px] flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">No Bed</label>
{{ billingNumber }} <label class="w-[20px] flex-none">:</label>
</DE.Field> <p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
</DE.Cell>
<DE.Cell>
<DE.Label class="font-semibold">Nama Ruang</DE.Label>
<DE.Field>
{{ roomName }}
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label class="font-semibold">No Bed</DE.Label>
<DE.Field>
{{ bedNumber }} {{ bedNumber }}
</DE.Field> </p>
</DE.Cell> </div>
<DE.Cell> <!-- Nama Pasien -->
<DE.Label class="font-semibold">DPJP</DE.Label> <div>
<DE.Field> <label class="text-sm font-semibold text-gray-700 dark:text-gray-300">Nama Pasien</label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ data.patient.person.name || '-' }}
</p>
</div>
<!-- Jenis Kelamin -->
<div>
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">Jenis Kelamin</label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ genderLabel }}
</p>
</div>
<!-- Alamat (spans 2 columns on lg) -->
<div class="lg:col-span-2">
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">Alamat</label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ address }}
</p>
</div>
<!-- Tanggal Masuk RS -->
<div>
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">Tanggal Masuk RS</label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ registeredDateFormatted }}
</p>
</div>
<!-- No. Billing -->
<div>
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">No. Billing</label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ billingNumber }}
</p>
</div>
<!-- Nama Ruang -->
<div>
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">Nama Ruang</label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ roomName }}
</p>
</div>
<!-- DPJP (spans 2 columns on lg) -->
<div class="lg:col-span-2">
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300">DPJP</label>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ dpjp }} {{ dpjp }}
</DE.Field> </p>
</DE.Cell> </div>
</DE.Block> </div>
</div> </div>
</template> </template>
+104 -26
View File
@@ -6,6 +6,7 @@ import { getDetail } from '~/services/encounter.service'
import { getPositionAs } from '~/lib/roles' import { getPositionAs } from '~/lib/roles'
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type' import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
import EncounterQuickInfoFull from '~/components/app/encounter/quick-info-full.vue'
import CompMenu from '~/components/pub/my-ui/comp-menu/comp-menu.vue' import CompMenu from '~/components/pub/my-ui/comp-menu/comp-menu.vue'
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue' import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
@@ -43,32 +44,80 @@ const activeTab = computed({
}) })
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0 const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
// const dataRes = await getDetail(id, {
// includes:
// 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person',
// })
// const dataResBody = dataRes.body ?? null
// const data = dataResBody?.data ?? null
// Dummy data so AppEncounterQuickInfo can render in development/storybook const data = ref<any>(null)
// Replace with real API result when available (see commented fetch below)
const data = ref<any>({ // Function to check if date is invalid (like "0001-01-01T00:00:00Z")
patient: { function isValidDate(dateString: string | null | undefined): boolean {
number: 'RM-2025-0001', if (!dateString) return false
person: { // Check for invalid date patterns
name: 'John Doe', if (dateString.startsWith('0001-01-01')) return false
birthDate: '1980-01-01T00:00:00Z', try {
gender_code: 'M', const date = new Date(dateString)
addresses: [{ address: 'Jl. Contoh No.1, Jakarta' }], return !isNaN(date.getTime())
frontTitle: '', } catch {
endTitle: '', return false
}
}
// Function to map API response to Encounter structure
function mapApiResponseToEncounter(apiResponse: any): any {
if (!apiResponse) return null
// Check if patient and patient.person exist (minimal validation)
if (!apiResponse.patient || !apiResponse.patient.person) {
return null
}
const mapped: any = {
id: apiResponse.id || 0,
patient_id: apiResponse.patient_id || apiResponse.patient?.id || 0,
patient: {
id: apiResponse.patient?.id || 0,
number: apiResponse.patient?.number || '',
person: {
id: apiResponse.patient?.person?.id || 0,
name: apiResponse.patient?.person?.name || '',
birthDate: apiResponse.patient?.person?.birthDate || null,
gender_code: apiResponse.patient?.person?.gender_code || '',
residentIdentityNumber: apiResponse.patient?.person?.residentIdentityNumber || null,
frontTitle: apiResponse.patient?.person?.frontTitle || '',
endTitle: apiResponse.patient?.person?.endTitle || '',
addresses: apiResponse.patient?.person?.addresses || [],
},
}, },
}, registeredAt: apiResponse.registeredAt || apiResponse.patient?.registeredAt || null,
visitDate: new Date().toISOString(), class_code: apiResponse.class_code || '',
unit: { name: 'Onkologi' }, unit_id: apiResponse.unit_id || 0,
responsible_doctor: null, unit: apiResponse.unit || null,
appointment_doctor: { employee: { person: { name: 'Dr. Clara Smith', frontTitle: 'Dr.', endTitle: 'Sp.OG' } } }, specialist_id: apiResponse.specialist_id || null,
}) subspecialist_id: apiResponse.subspecialist_id || null,
visitDate: isValidDate(apiResponse.visitDate) ? apiResponse.visitDate : (apiResponse.registeredAt || apiResponse.patient?.registeredAt || null),
adm_employee_id: apiResponse.adm_employee_id || 0,
appointment_doctor_id: apiResponse.appointment_doctor_id || null,
responsible_doctor_id: apiResponse.responsible_doctor_id || null,
appointment_doctor: apiResponse.appointment_doctor || null,
responsible_doctor: apiResponse.responsible_doctor || null,
refSource_name: apiResponse.refSource_name || null,
appointment_id: apiResponse.appointment_id || null,
earlyEducation: apiResponse.earlyEducation || null,
medicalDischargeEducation: apiResponse.medicalDischargeEducation || '',
admDischargeEducation: apiResponse.admDischargeEducation || null,
discharge_method_code: apiResponse.discharge_method_code || null,
discharge_reason: apiResponse.dischargeReason || apiResponse.discharge_reason || null,
discharge_date: apiResponse.discharge_date || null,
status_code: apiResponse.status_code || '',
// Payment related fields
paymentMethod_code: apiResponse.paymentMethod_code && apiResponse.paymentMethod_code.trim() !== ''
? apiResponse.paymentMethod_code
: null,
trx_number: apiResponse.trx_number || null,
member_number: apiResponse.member_number || null,
ref_number: apiResponse.ref_number || null,
}
return mapped
}
// Dummy rows for ProtocolList (matches keys expected by list-cfg.protocol) // Dummy rows for ProtocolList (matches keys expected by list-cfg.protocol)
const protocolRows = [ const protocolRows = [
@@ -289,6 +338,31 @@ const tabsRaws: TabItem[] = [
}, },
] ]
async function getData() {
try {
const dataRes = await getDetail(id, {
includes:
'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person,Responsible_Doctor,Responsible_Doctor-employee,Responsible_Doctor-employee-person',
})
const dataResBody = dataRes.body ?? null
const result = dataResBody?.data ?? null
if (result) {
const mappedData = mapApiResponseToEncounter(result)
if (mappedData) {
data.value = mappedData
} else {
data.value = null
}
} else {
data.value = null
}
} catch (error) {
console.error('Error fetching encounter data:', error)
data.value = null
}
}
function getMenus() { function getMenus() {
return tabsRaws return tabsRaws
.filter((tab: TabItem) => (tab.groups ? tab.groups.some((group: string) => group === activePosition.value) : false)) .filter((tab: TabItem) => (tab.groups ? tab.groups.some((group: string) => group === activePosition.value) : false))
@@ -307,7 +381,8 @@ watch(getActiveRole, () => {
tabs.value = getMenus() tabs.value = getMenus()
}) })
onMounted(() => { onMounted(async () => {
await getData()
tabs.value = getMenus() tabs.value = getMenus()
}) })
</script> </script>
@@ -317,7 +392,10 @@ onMounted(() => {
<div class="mb-4"> <div class="mb-4">
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" /> <PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" />
</div> </div>
<AppEncounterQuickInfo :data="data" /> <EncounterQuickInfoFull
v-if="data && data.patient && data.patient.person"
:data="data"
/>
<CompTab <CompTab
v-if="currentDisplay === 'tab'" v-if="currentDisplay === 'tab'"
:data="tabs" :data="tabs"
+1
View File
@@ -36,6 +36,7 @@ export interface Encounter {
discharge_date?: string discharge_date?: string
internalReferences?: InternalReference[] internalReferences?: InternalReference[]
deathCause?: DeathCause deathCause?: DeathCause
paymentMethod_code?: string
status_code: string status_code: string
} }
@@ -0,0 +1,44 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah Kunjungan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/outpatient/encounter']
const { checkRole, hasCreateAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
throw createError({
statusCode: 403,
statusMessage: 'Access denied',
})
}
// Define permission-based computed properties
const canCreate = hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentEncounterHome display="menu" class-code="ambulatory" sub-class-code="reg" />
</div>
<Error
v-else
:status-code="403"
/>
</template>