fix: improves encounter detail rendering and data mapping
This commit is contained in:
@@ -75,7 +75,7 @@ const genderLabel = computed(() => {
|
|||||||
const paymentTypeLabel = computed(() => {
|
const paymentTypeLabel = computed(() => {
|
||||||
const code = props.data.paymentMethod_code
|
const code = props.data.paymentMethod_code
|
||||||
if (!code) return '-'
|
if (!code) return '-'
|
||||||
|
|
||||||
// Map payment method codes
|
// Map payment method codes
|
||||||
if (code === 'insurance') {
|
if (code === 'insurance') {
|
||||||
return 'JKN'
|
return 'JKN'
|
||||||
@@ -88,12 +88,12 @@ const paymentTypeLabel = computed(() => {
|
|||||||
} else if (code === 'pks') {
|
} else if (code === 'pks') {
|
||||||
return 'PKS'
|
return 'PKS'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get from paymentTypes constant
|
// Try to get from paymentTypes constant
|
||||||
if (paymentTypes[code]) {
|
if (paymentTypes[code]) {
|
||||||
return paymentTypes[code].split(' ')[0] // Get first part (e.g., "JKN" from "JKN (Jaminan...)")
|
return paymentTypes[code].split(' ')[0] // Get first part (e.g., "JKN" from "JKN (Jaminan...)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return code
|
return code
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user