feat(encounter): implement encounter detail loading and update handling in entry form

This commit is contained in:
riefive
2025-11-10 12:44:27 +07:00
parent a8bc3647a1
commit b52f438b23
6 changed files with 404 additions and 36 deletions
+158 -7
View File
@@ -18,7 +18,7 @@ import {
getValueTreeItems as getSpecialistTreeItems,
} from '~/services/specialist.service'
import { getValueLabelList as getDoctorValueLabelList } from '~/services/doctor.service'
import { create as createEncounter } from '~/services/encounter.service'
import { create as createEncounter, getDetail as getEncounterDetail, update as updateEncounter } from '~/services/encounter.service'
// Handlers
import {
@@ -55,11 +55,17 @@ const specialistsData = ref<any[]>([]) // Store full specialist data with id
const doctorsList = ref<Array<{ value: string; label: string }>>([])
const recSelectId = ref<number | null>(null)
const isSaving = ref(false)
const isLoadingDetail = ref(false)
const formRef = ref<InstanceType<typeof AppEncounterEntryForm> | null>(null)
const encounterData = ref<any>(null)
const formObjects = ref<any>({})
// Computed for edit mode
const isEditMode = computed(() => props.id > 0)
// Computed for save button disable state
const isSaveDisabled = computed(() => {
return !selectedPatient.value || !selectedPatientObject.value || isSaving.value
return !selectedPatient.value || !selectedPatientObject.value || isSaving.value || isLoadingDetail.value
})
function getListPath(): string {
@@ -186,19 +192,24 @@ async function handleSaveEncounter(formValues: any) {
payload.allocatedVisitCount = 0
}
// Call encounter service
const result = await createEncounter(payload)
// Call encounter service - use update if edit mode, create otherwise
let result
if (isEditMode.value) {
result = await updateEncounter(props.id, payload)
} else {
result = await createEncounter(payload)
}
if (result.success) {
toast({
title: 'Berhasil',
description: 'Kunjungan berhasil dibuat',
description: isEditMode.value ? 'Kunjungan berhasil diperbarui' : 'Kunjungan berhasil dibuat',
variant: 'default',
})
// Redirect to list page
await navigateTo(getListPath())
} else {
const errorMessage = result.body?.message || 'Gagal membuat kunjungan'
const errorMessage = result.body?.message || (isEditMode.value ? 'Gagal memperbarui kunjungan' : 'Gagal membuat kunjungan')
toast({
title: 'Gagal',
description: errorMessage,
@@ -209,7 +220,7 @@ async function handleSaveEncounter(formValues: any) {
console.error('Error saving encounter:', error)
toast({
title: 'Gagal',
description: error?.message || 'Gagal membuat kunjungan',
description: error?.message || (isEditMode.value ? 'Gagal memperbarui kunjungan' : 'Gagal membuat kunjungan'),
variant: 'destructive',
})
} finally {
@@ -370,11 +381,149 @@ async function handleInit() {
await handleFetchSpecialists()
}
/**
* Load encounter detail data for edit mode
*/
async function loadEncounterDetail() {
if (!isEditMode.value || props.id <= 0) {
return
}
try {
isLoadingDetail.value = true
const result = await getEncounterDetail(props.id, {
includes: 'patient,patient-person,specialist,subspecialist,appointment_doctor,responsible_doctor,encounter_payments',
})
if (result.success && result.body?.data) {
encounterData.value = result.body.data
await mapEncounterToForm(encounterData.value)
} else {
toast({
title: 'Gagal',
description: 'Gagal memuat data kunjungan',
variant: 'destructive',
})
// Redirect to list page if encounter not found
await navigateTo(getListPath())
}
} catch (error: any) {
console.error('Error loading encounter detail:', error)
toast({
title: 'Gagal',
description: error?.message || 'Gagal memuat data kunjungan',
variant: 'destructive',
})
// Redirect to list page on error
await navigateTo(getListPath())
} finally {
isLoadingDetail.value = false
}
}
/**
* Map encounter data to form fields
*/
async function mapEncounterToForm(encounter: any) {
if (!encounter) return
// Set patient data and wait for it to load
if (encounter.patient) {
selectedPatient.value = String(encounter.patient.id)
selectedPatientObject.value = encounter.patient
// Fetch full patient data to ensure we have all fields
await getPatientCurrent(selectedPatient.value)
}
// Map form fields
const formData: any = {}
// Patient data (readonly, populated from selected patient)
// Use selectedPatientObject which now has full patient data
if (selectedPatientObject.value?.person) {
formData.patientName = selectedPatientObject.value.person.name || ''
formData.nationalIdentity = selectedPatientObject.value.person.residentIdentityNumber || ''
formData.medicalRecordNumber = selectedPatientObject.value.number || ''
} else if (encounter.patient?.person) {
// Fallback to encounter patient data if selectedPatientObject is not yet loaded
formData.patientName = encounter.patient.person.name || ''
formData.nationalIdentity = encounter.patient.person.residentIdentityNumber || ''
formData.medicalRecordNumber = encounter.patient.number || ''
}
// Doctor ID
const doctorId = encounter.appointment_doctor_id || encounter.responsible_doctor_id
if (doctorId) {
formData.doctorId = String(doctorId)
}
// Specialist/Subspecialist
if (encounter.subspecialist?.code) {
formData.subSpecialistId = encounter.subspecialist.code
} else if (encounter.specialist?.code) {
formData.subSpecialistId = encounter.specialist.code
}
// Register date
if (encounter.registeredAt) {
// Convert ISO date to local date string (YYYY-MM-DD)
const date = new Date(encounter.registeredAt)
formData.registerDate = date.toISOString().split('T')[0]
} else if (encounter.visitDate) {
const date = new Date(encounter.visitDate)
formData.registerDate = date.toISOString().split('T')[0]
}
// Payment data
// Check if encounter has payment data
if (encounter.encounter_payments && Array.isArray(encounter.encounter_payments) && encounter.encounter_payments.length > 0) {
const payment = encounter.encounter_payments[0]
// Determine payment type from paymentMethod_code
if (payment.paymentMethod_code === 'insurance') {
// Check if it's JKN or JKMM based on member_number or other indicators
// For now, default to 'jkn' - this might need adjustment based on actual data structure
formData.paymentType = 'jkn'
formData.cardNumber = payment.member_number || ''
formData.sepNumber = payment.ref_number || ''
// Note: patientCategory and sepType might need to be extracted from other sources
// as they might not be directly in the payment object
} else {
// For non-insurance payments, try to determine from encounter data
// This might need adjustment based on actual API response
formData.paymentType = 'spm' // default to SPM
}
} else {
// Fallback: try to get payment type from encounter.paymentType if available
if (encounter.paymentType) {
formData.paymentType = encounter.paymentType
}
if (encounter.cardNumber) {
formData.cardNumber = encounter.cardNumber
}
if (encounter.ref_number || encounter.sepNumber) {
formData.sepNumber = encounter.ref_number || encounter.sepNumber
}
}
// Set form objects for the form component
formObjects.value = formData
// Fetch doctors based on specialist/subspecialist selection
if (formData.subSpecialistId) {
await handleFetchDoctors(formData.subSpecialistId)
}
}
provide('rec_select_id', recSelectId)
provide('table_data_loader', isLoading)
onMounted(async () => {
await handleInit()
// Load encounter detail if in edit mode
if (isEditMode.value) {
await loadEncounterDetail()
}
})
</script>
@@ -396,6 +545,8 @@ onMounted(async () => {
:specialists="specialistsTree"
:doctor="doctorsList"
:patient="selectedPatientObject"
:objects="formObjects"
:is-loading="isLoadingDetail"
@event="handleEvent"
@fetch="handleFetch"
/>
+58 -23
View File
@@ -74,6 +74,25 @@ const refSearchNav: RefSearchNav = {
// Loading state management
/**
* Get base path for encounter routes based on classCode and subClassCode
*/
function getBasePath(): string {
if (props.classCode === 'ambulatory' && props.subClassCode === 'rehab') {
return '/rehab/encounter'
}
if (props.classCode === 'ambulatory' && props.subClassCode === 'reg') {
return '/outpatient/encounter'
}
if (props.classCode === 'emergency') {
return '/emergency/encounter'
}
if (props.classCode === 'inpatient') {
return '/inpatient/encounter'
}
return '/encounter' // fallback
}
async function getPatientList() {
isLoading.isTableLoading = true
try {
@@ -81,6 +100,9 @@ async function getPatientList() {
if (props.classCode) {
params['class-code'] = props.classCode
}
if (props.subClassCode) {
params['sub-class-code'] = props.subClassCode
}
const result = await getEncounterList(params)
if (result.success) {
data.value = result.body?.data || []
@@ -144,27 +166,31 @@ watch(
isRecordConfirmationOpen.value = true
return
}
// if (props.type === 'encounter') {
// if (recAction.value === 'showDetail') {
// navigateTo(`/rehab/encounter/${recId.value}/detail`)
// } else if (recAction.value === 'showEdit') {
// navigateTo(`/rehab/encounter/${recId.value}/edit`)
// } else if (recAction.value === 'showProcess') {
// navigateTo(`/rehab/encounter/${recId.value}/process`)
// } else {
// // handle other actions
// }
// } else if (props.type === 'registration') {
// if (recAction.value === 'showDetail') {
// navigateTo(`/rehab/registration/${recId.value}/detail`)
// } else if (recAction.value === 'showEdit') {
// navigateTo(`/rehab/registration/${recId.value}/edit`)
// } else if (recAction.value === 'showProcess') {
// navigateTo(`/rehab/registration/${recId.value}/process`)
// } else {
// // handle other actions
// }
// }
const basePath = getBasePath()
if (props.type === 'encounter') {
if (recAction.value === 'showDetail') {
navigateTo(`${basePath}/${recId.value}/detail`)
} else if (recAction.value === 'showEdit') {
navigateTo(`${basePath}/${recId.value}/edit`)
} else if (recAction.value === 'showProcess') {
navigateTo(`${basePath}/${recId.value}/process`)
} else {
// handle other actions
}
} else if (props.type === 'registration') {
// Handle registration type if needed
if (recAction.value === 'showDetail') {
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/detail`)
} else if (recAction.value === 'showEdit') {
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/edit`)
} else if (recAction.value === 'showProcess') {
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/process`)
} else {
// handle other actions
}
}
},
)
@@ -185,7 +211,10 @@ onMounted(() => {
/>
<Separator class="my-4 xl:my-5" />
<Filter :ref-search-nav="refSearchNav" />
<Filter
:prep="hreaderPrep"
:ref-search-nav="refSearchNav"
/>
<AppEncounterList :data="data" />
@@ -195,7 +224,13 @@ onMounted(() => {
size="lg"
prevent-outside
>
<AppEncounterFilter />
<AppEncounterFilter
:installation="{
msg: { placeholder: 'Pilih' },
items: [],
}"
:schema="{}"
/>
</Dialog>
<!-- Record Confirmation Modal -->
@@ -0,0 +1,56 @@
<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: 'Edit 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['/emergency/encounter']
const { checkRole, hasUpdateAccess } = 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 canUpdate = hasUpdateAccess(roleAccess)
// Get encounter ID from route params
const encounterId = computed(() => {
const id = route.params.id
return typeof id === 'string' ? parseInt(id) : 0
})
</script>
<template>
<div v-if="canUpdate">
<ContentEncounterEntry
:id="encounterId"
class-code="emergency"
sub-class-code="emg"
form-type="Edit"
/>
</div>
<Error
v-else
:status-code="403"
/>
</template>
@@ -0,0 +1,56 @@
<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: 'Edit 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['/inpatient/encounter']
const { checkRole, hasUpdateAccess } = 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 canUpdate = hasUpdateAccess(roleAccess)
// Get encounter ID from route params
const encounterId = computed(() => {
const id = route.params.id
return typeof id === 'string' ? parseInt(id) : 0
})
</script>
<template>
<div v-if="canUpdate">
<ContentEncounterEntry
:id="encounterId"
class-code="inpatient"
sub-class-code="icu"
form-type="Edit"
/>
</div>
<Error
v-else
:status-code="403"
/>
</template>
@@ -0,0 +1,56 @@
<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: 'Edit 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, hasUpdateAccess } = 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 canUpdate = hasUpdateAccess(roleAccess)
// Get encounter ID from route params
const encounterId = computed(() => {
const id = route.params.id
return typeof id === 'string' ? parseInt(id) : 0
})
</script>
<template>
<div v-if="canUpdate">
<ContentEncounterEntry
:id="encounterId"
class-code="ambulatory"
sub-class-code="reg"
form-type="Edit"
/>
</div>
<Error
v-else
:status-code="403"
/>
</template>
@@ -6,7 +6,7 @@ import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah Kunjungan',
title: 'Edit Kunjungan',
contentFrame: 'cf-full-width',
})
@@ -18,7 +18,7 @@ useHead({
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const { checkRole, hasCreateAccess } = useRBAC()
const { checkRole, hasUpdateAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
@@ -30,12 +30,26 @@ if (!hasAccess) {
}
// Define permission-based computed properties
const canCreate = hasCreateAccess(roleAccess)
const canUpdate = hasUpdateAccess(roleAccess)
// Get encounter ID from route params
const encounterId = computed(() => {
const id = route.params.id
return typeof id === 'string' ? parseInt(id) : 0
})
</script>
<template>
<div v-if="canCreate">
<ContentEncounterEntry :id="1" form-type="Edit" />
<div v-if="canUpdate">
<ContentEncounterEntry
:id="encounterId"
class-code="ambulatory"
sub-class-code="rehab"
form-type="Edit"
/>
</div>
<Error v-else :status-code="403" />
<Error
v-else
:status-code="403"
/>
</template>