Merge branch 'dev' of https://github.com/dikstub-rssa/simrs-fe into feeat/pendaftaran-kemoterapi-141

This commit is contained in:
riefive
2025-11-13 10:14:17 +07:00
244 changed files with 14473 additions and 2258 deletions
@@ -1,3 +0,0 @@
<template>
<div>halo</div>
</template>
@@ -1,64 +0,0 @@
<script setup lang="ts">
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import AssesmentFunctionList from '~/components/app/encounter/assesment-function/list.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
const props = defineProps<{
label: string
}>()
const data = ref([])
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (_val: string) => {
// filter patient list
},
onClear: () => {
// clear url param
},
}
// Loading state management
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
})
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const hreaderPrep: HeaderPrep = {
title: props.label,
icon: 'i-lucide-users',
addNav: {
label: 'Tambah',
onClick: () => navigateTo('/rehab/registration-queue/sep-prosedur/add'),
},
}
async function getPatientList() {
const resp = await xfetch('/api/v1/patient')
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
}
}
onMounted(() => {
getPatientList()
})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
</script>
<template>
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
<AssesmentFunctionList :data="data" />
</div>
</template>
+750 -26
View File
@@ -1,51 +1,775 @@
<script setup lang="ts">
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
// Components
import { toast } from '~/components/pub/ui/toast'
import { Button } from '~/components/pub/ui/button'
import AppEncounterEntryForm from '~/components/app/encounter/entry-form.vue'
import AppViewPatient from '~/components/app/patient/view-patient.vue'
// Types
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
// Constants
import { paymentTypes, sepRefTypeCodes, participantGroups } from '~/lib/constants.vclaim'
// Services
import {
getList as getSpecialistList,
getValueTreeItems as getSpecialistTreeItems,
} from '~/services/specialist.service'
import { getValueLabelList as getDoctorValueLabelList } from '~/services/doctor.service'
import { create as createEncounter, getDetail as getEncounterDetail, update as updateEncounter } from '~/services/encounter.service'
import { getList as getSepList } from '~/services/vclaim-sep.service'
// Helpers
import { refDebounced } from '@vueuse/core'
// Handlers
import {
patients,
selectedPatient,
selectedPatientObject,
paginationMeta,
getPatientsList,
getPatientCurrent,
getPatientByIdentifierSearch,
} from '~/handlers/patient.handler'
// Stores
import { useUserStore } from '~/stores/user'
const props = defineProps<{
id: number
classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient'
subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
formType: string
}>()
const isOpen = ref(false)
const data = ref([])
const route = useRoute()
const userStore = useUserStore()
const openPatient = ref(false)
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
})
const paymentsList = ref<Array<{ value: string; label: string }>>([])
const sepsList = ref<Array<{ value: string; label: string }>>([])
const participantGroupsList = ref<Array<{ value: string; label: string }>>([])
const specialistsTree = ref<TreeItem[]>([])
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>({})
async function getPatientList() {
isLoading.isTableLoading = true
const resp = await xfetch('/api/v1/patient')
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
}
isLoading.isTableLoading = false
}
// SEP validation state
const isSepValid = ref(false)
const isCheckingSep = ref(false)
const sepNumber = ref('')
const debouncedSepNumber = refDebounced(sepNumber, 500)
onMounted(() => {
getPatientList()
// 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 || isLoadingDetail.value
})
function onClick(e: 'search' | 'add') {
console.log('click', e)
if (e === 'search') {
isOpen.value = true
} else if (e === 'add') {
navigateTo('/client/patient/add')
function getListPath(): 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
}
function handleSavePatient() {
selectedPatientObject.value = null
setTimeout(() => {
getPatientCurrent(selectedPatient.value)
}, 150)
}
function toKebabCase(str: string): string {
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}
function toNavigateSep(values: any) {
const queryParams = new URLSearchParams()
if (values['subSpecialistCode']) {
const isSub = isSubspecialist(values['subSpecialistCode'], specialistsTree.value)
if (!isSub) {
values['specialistCode'] = values['subSpecialistCode']
delete values['subSpecialistCode']
}
}
Object.keys(values).forEach((field) => {
if (values[field]) {
queryParams.append(toKebabCase(field), values[field])
}
})
navigateTo('/integration/bpjs/sep/add' + `?${queryParams.toString()}`)
}
async function handleSaveEncounter(formValues: any) {
// Validate patient is selected
if (!selectedPatient.value || !selectedPatientObject.value) {
toast({
title: 'Gagal',
description: 'Pasien harus dipilih terlebih dahulu',
variant: 'destructive',
})
return
}
try {
isSaving.value = true
// Get employee_id from user store
const employeeId = userStore.user?.employee_id || userStore.user?.employee?.id || 0
// Format date to ISO format
const formatDate = (dateString: string): string => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toISOString()
}
// Get specialist_id and subspecialist_id from TreeSelect value (code)
const { specialist_id, subspecialist_id } = getSpecialistIdsFromCode(formValues.subSpecialistId || '')
// Build payload
const payload: any = {
patient_id: selectedPatientObject.value?.id || Number(selectedPatient.value),
registeredAt: formatDate(formValues.registerDate),
visitDate: formatDate(formValues.registerDate),
class_code: props.classCode || '',
subClass_code: props.subClassCode || '',
infra_id: null,
unit_id: null,
appointment_doctor_id: Number(formValues.doctorId),
responsible_doctor_id: Number(formValues.doctorId),
paymentType: formValues.paymentType,
cardNumber: formValues.cardNumber,
refSource_name: '',
appointment_id: null,
}
if (employeeId && employeeId > 0) {
payload.adm_employee_id = employeeId
}
// Add specialist_id and subspecialist_id if available
if (specialist_id) {
payload.specialist_id = specialist_id
}
if (subspecialist_id) {
payload.subspecialist_id = subspecialist_id
}
let paymentMethod = 'cash'
if (formValues.paymentType === 'jkn' || formValues.paymentType === 'jkmm') {
paymentMethod = 'insurance'
} else if (formValues.paymentType === 'spm') {
paymentMethod = 'cash'
} else if (formValues.paymentType === 'pks') {
paymentMethod = 'membership'
}
if (paymentMethod === 'insurance') {
payload.paymentMethod_code = paymentMethod
payload.insuranceCompany_id = null
payload.member_number = formValues.cardNumber
payload.ref_number = formValues.sepNumber
}
// Add visitMode_code and allocatedVisitCount only if classCode is ambulatory
if (props.classCode === 'ambulatory') {
payload.visitMode_code = 'adm'
payload.allocatedVisitCount = 0
}
// 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: isEditMode.value ? 'Kunjungan berhasil diperbarui' : 'Kunjungan berhasil dibuat',
variant: 'default',
})
// Redirect to list page
await navigateTo(getListPath())
} else {
const errorMessage = result.body?.message || (isEditMode.value ? 'Gagal memperbarui kunjungan' : 'Gagal membuat kunjungan')
toast({
title: 'Gagal',
description: errorMessage,
variant: 'destructive',
})
}
} catch (error: any) {
console.error('Error saving encounter:', error)
toast({
title: 'Gagal',
description: error?.message || (isEditMode.value ? 'Gagal memperbarui kunjungan' : 'Gagal membuat kunjungan'),
variant: 'destructive',
})
} finally {
isSaving.value = false
}
}
async function handleEvent(menu: string, value?: any) {
if (menu === 'search') {
getPatientsList({ 'page-size': 10, includes: 'person' }).then(() => {
openPatient.value = true
})
} else if (menu === 'add') {
navigateTo('/client/patient/add')
} else if (menu === 'add-sep') {
// If SEP is already valid, don't navigate
if (isSepValid.value) {
return
}
recSelectId.value = null
toNavigateSep({
isService: 'false',
sourcePath: route.path,
resource: `${props.classCode}-${props.subClassCode}`,
...value,
})
} else if (menu === 'sep-number-changed') {
// Update sepNumber when it changes in form (only if different to prevent loop)
if (sepNumber.value !== value) {
sepNumber.value = value || ''
}
} else if (menu === 'save') {
await handleSaveEncounter(value)
} else if (menu === 'cancel') {
navigateTo(getListPath())
}
}
// Handle save button click
function handleSaveClick() {
console.log('🔵 handleSaveClick called')
console.log('🔵 formRef:', formRef.value)
console.log('🔵 isSaveDisabled:', isSaveDisabled.value)
console.log('🔵 selectedPatient:', selectedPatient.value)
console.log('🔵 selectedPatientObject:', selectedPatientObject.value)
console.log('🔵 isSaving:', isSaving.value)
console.log('🔵 isLoadingDetail:', isLoadingDetail.value)
if (formRef.value && typeof formRef.value.submitForm === 'function') {
console.log('🔵 Calling formRef.value.submitForm()')
formRef.value.submitForm()
} else {
console.error('❌ formRef.value is null or submitForm is not a function')
}
}
/**
* Validate SEP number
*/
async function validateSepNumber(sepNumberValue: string) {
// Reset validation if SEP number is empty
if (!sepNumberValue || sepNumberValue.trim() === '') {
isSepValid.value = false
isCheckingSep.value = false
return
}
// Only check if payment type is JKN
// We need to check from formObjects
const paymentType = formObjects.value?.paymentType
if (paymentType !== 'jkn') {
isSepValid.value = false
return
}
try {
isCheckingSep.value = true
const result = await getSepList({ number: sepNumberValue.trim() })
// Check if SEP is found
// If response is not null, SEP is found
// If response is null with metaData code "201", SEP is not found
if (result.success && result.body?.response !== null) {
isSepValid.value = true
} else {
// SEP not found (response null with metaData code "201")
isSepValid.value = false
}
} catch (error) {
console.error('Error checking SEP:', error)
isSepValid.value = false
} finally {
isCheckingSep.value = false
}
}
// Watch debounced SEP number to validate
watch(debouncedSepNumber, async (newValue) => {
await validateSepNumber(newValue)
})
// Watch payment type to reset SEP validation
watch(
() => formObjects.value?.paymentType,
(newValue) => {
isSepValid.value = false
// If payment type is not JKN, clear SEP number
if (newValue !== 'jkn') {
sepNumber.value = ''
}
},
)
async function handleFetchSpecialists() {
try {
const specialistsResult = await getSpecialistList({ 'page-size': 100, includes: 'subspecialists' })
if (specialistsResult.success) {
const specialists = specialistsResult.body?.data || []
specialistsData.value = specialists // Store full data for mapping
specialistsTree.value = getSpecialistTreeItems(specialists)
}
} catch (error) {
console.error('Error fetching specialist-subspecialist tree:', error)
}
}
/**
* Helper function to check if a value exists in the specialistsTree
* Returns true if it's a leaf node (subspecialist), false if parent node (specialist)
*/
function isSubspecialist(value: string, items: TreeItem[]): boolean {
for (const item of items) {
if (item.value === value) {
// If this item has children, it's not selected, so skip
// If this is the selected item, check if it has children in the tree
return false // This means it's a specialist, not a subspecialist
}
if (item.children) {
for (const child of item.children) {
if (child.value === value) {
// This is a subspecialist (leaf node)
return true
}
}
}
}
return false
}
/**
* Helper function to get specialist/subspecialist code from ID
* Returns code string or null if not found
*/
function getSpecialistCodeFromId(id: number | null | undefined): string | null {
if (!id) return null
// First check if encounter has specialist object with code
if (encounterData.value?.specialist?.id === id) {
return encounterData.value.specialist.code || null
}
// Search in specialistsData
for (const specialist of specialistsData.value) {
if (specialist.id === id) {
return specialist.code || null
}
// Check subspecialists
if (specialist.subspecialists && Array.isArray(specialist.subspecialists)) {
for (const subspecialist of specialist.subspecialists) {
if (subspecialist.id === id) {
return subspecialist.code || null
}
}
}
}
return null
}
/**
* Helper function to get subspecialist code from ID
* Returns code string or null if not found
*/
function getSubspecialistCodeFromId(id: number | null | undefined): string | null {
if (!id) return null
// First check if encounter has subspecialist object with code
if (encounterData.value?.subspecialist?.id === id) {
return encounterData.value.subspecialist.code || null
}
// Search in specialistsData
for (const specialist of specialistsData.value) {
if (specialist.subspecialists && Array.isArray(specialist.subspecialists)) {
for (const subspecialist of specialist.subspecialists) {
if (subspecialist.id === id) {
return subspecialist.code || null
}
}
}
}
return null
}
/**
* Helper function to find specialist_id and subspecialist_id from TreeSelect value (code)
* Returns { specialist_id: number | null, subspecialist_id: number | null }
*/
function getSpecialistIdsFromCode(code: string): { specialist_id: number | null; subspecialist_id: number | null } {
if (!code) {
return { specialist_id: null, subspecialist_id: null }
}
// Check if it's a subspecialist
const isSub = isSubspecialist(code, specialistsTree.value)
if (isSub) {
// Find subspecialist and its parent specialist
for (const specialist of specialistsData.value) {
if (specialist.subspecialists && Array.isArray(specialist.subspecialists)) {
for (const subspecialist of specialist.subspecialists) {
if (subspecialist.code === code) {
return {
specialist_id: specialist.id ? Number(specialist.id) : null,
subspecialist_id: subspecialist.id ? Number(subspecialist.id) : null,
}
}
}
}
}
} else {
// It's a specialist
for (const specialist of specialistsData.value) {
if (specialist.code === code) {
return {
specialist_id: specialist.id ? Number(specialist.id) : null,
subspecialist_id: null,
}
}
}
}
return { specialist_id: null, subspecialist_id: null }
}
async function handleFetchDoctors(subSpecialistId: string | null = null) {
try {
// Build filter based on selection type
const filterParams: any = { 'page-size': 100, includes: 'employee-Person' }
if (!subSpecialistId) {
const doctors = await getDoctorValueLabelList(filterParams)
doctorsList.value = doctors
return
}
// Check if the selectd value is a subspecialist or specialist
const isSub = isSubspecialist(subSpecialistId, specialistsTree.value)
if (isSub) {
// If selected is subspecialist, filter by subspecialist-id
filterParams['subspecialist-id'] = subSpecialistId
} else {
// If selected is specialist, filter by specialist-id
filterParams['specialist-id'] = subSpecialistId
}
const doctors = await getDoctorValueLabelList(filterParams)
doctorsList.value = doctors
} catch (error) {
console.error('Error fetching doctors:', error)
doctorsList.value = []
}
}
function handleFetch(value?: any) {
if (value?.subSpecialistId) {
// handleFetchDoctors(value.subSpecialistId)
}
}
async function handleInit() {
selectedPatientObject.value = null
paymentsList.value = Object.keys(paymentTypes).map((item) => ({
value: item.toString(),
label: paymentTypes[item],
})) as any
sepsList.value = Object.keys(sepRefTypeCodes).map((item) => ({
value: item.toString(),
label: sepRefTypeCodes[item],
})) as any
participantGroupsList.value = Object.keys(participantGroups).map((item) => ({
value: item.toString(),
label: participantGroups[item],
})) as any
// Fetch tree data
await handleFetchDoctors()
await handleFetchSpecialists()
}
/**
* Load encounter detail data for edit mode
*/
async function loadEncounterDetail() {
if (!isEditMode.value || props.id <= 0) {
return
}
try {
isLoadingDetail.value = true
// Include patient, person, specialist, and subspecialist in the response
const result = await getEncounterDetail(props.id, {
includes: 'patient,patient-person,specialist,subspecialist',
})
if (result.success && result.body?.data) {
encounterData.value = result.body.data
await mapEncounterToForm(encounterData.value)
// Set loading to false after mapping is complete
isLoadingDetail.value = false
} 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 - use data from response if available
if (encounter.patient) {
selectedPatient.value = String(encounter.patient.id)
selectedPatientObject.value = encounter.patient
// Only fetch patient if person data is missing
if (!encounter.patient.person) {
await getPatientCurrent(selectedPatient.value)
}
}
// Map form fields
const formData: any = {}
// Patient data (readonly, populated from selected patient)
// Use encounter.patient.person which is already in the response
if (encounter.patient?.person) {
formData.patientName = encounter.patient.person.name || ''
formData.nationalIdentity = encounter.patient.person.residentIdentityNumber || ''
formData.medicalRecordNumber = encounter.patient.number || ''
} else if (selectedPatientObject.value?.person) {
// Fallback to selectedPatientObject if encounter.patient.person is not available
formData.patientName = selectedPatientObject.value.person.name || ''
formData.nationalIdentity = selectedPatientObject.value.person.residentIdentityNumber || ''
formData.medicalRecordNumber = selectedPatientObject.value.number || ''
}
// Doctor ID
const doctorId = encounter.appointment_doctor_id || encounter.responsible_doctor_id
if (doctorId) {
formData.doctorId = String(doctorId)
}
// Specialist/Subspecialist - use helper function to get code from ID
// Priority: subspecialist_id > specialist_id
if (encounter.subspecialist_id) {
const subspecialistCode = getSubspecialistCodeFromId(encounter.subspecialist_id)
if (subspecialistCode) {
formData.subSpecialistId = subspecialistCode
}
} else if (encounter.specialist_id) {
const specialistCode = getSpecialistCodeFromId(encounter.specialist_id)
if (specialistCode) {
formData.subSpecialistId = specialistCode
}
}
// Fallback: if encounter has specialist/subspecialist object with code
if (!formData.subSpecialistId) {
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 - use fields directly from encounter
// Map paymentMethod_code to paymentType
if (encounter.paymentMethod_code) {
// Map paymentMethod_code to paymentType
// 'insurance' typically means JKN/JKMM
if (encounter.paymentMethod_code === 'insurance') {
formData.paymentType = 'jkn' // Default to JKN for insurance
} else {
// For other payment methods, use the code directly if it matches
// Otherwise default to 'spm'
const validPaymentTypes = ['jkn', 'jkmm', 'spm', 'pks']
if (validPaymentTypes.includes(encounter.paymentMethod_code)) {
formData.paymentType = encounter.paymentMethod_code
} else {
formData.paymentType = 'spm' // Default to SPM
}
}
} else {
// If paymentMethod_code is empty or null, default to 'spm'
formData.paymentType = 'spm'
}
// Map payment fields directly from encounter
formData.cardNumber = encounter.member_number || ''
formData.sepNumber = encounter.ref_number || ''
// Note: patientCategory and sepType might not be available in the response
// These fields might need to be set manually or fetched from other sources
// Set form objects for the form component
formObjects.value = formData
// Update sepNumber for validation
if (formData.sepNumber) {
sepNumber.value = formData.sepNumber
}
// 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>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<Icon name="i-lucide-user" class="me-2" />
<span class="font-semibold">{{ props.formType }}</span> Kunjungan
<Icon
name="i-lucide-user"
class="me-2"
/>
<span class="font-semibold">{{ props.formType }}</span>
Kunjungan
</div>
<AppEncounterEntryForm @click="onClick" />
<AppSepSmallEntry v-if="props.id" />
<Dialog v-model:open="isOpen" title="Cari Pasien" size="xl" prevent-outside>
<AppPatientPicker :data="data" />
</Dialog>
<AppEncounterEntryForm
ref="formRef"
:is-loading="isLoadingDetail"
:is-sep-valid="isSepValid"
:is-checking-sep="isCheckingSep"
:payments="paymentsList"
:seps="sepsList"
:participant-groups="participantGroupsList"
:specialists="specialistsTree"
:doctor="doctorsList"
:patient="selectedPatientObject"
:objects="formObjects"
@event="handleEvent"
@fetch="handleFetch"
/>
<AppViewPatient
v-model:open="openPatient"
v-model:selected="selectedPatient"
:patients="patients"
:pagination-meta="paginationMeta"
@fetch="
(value) => {
if (value.search && value.search.length >= 3) {
// Use identifier search for specific searches (NIK, RM, etc.)
getPatientByIdentifierSearch(value.search)
} else {
// Use regular search for general searches
getPatientsList({ ...value, 'page-size': 10, includes: 'person' })
}
}
"
@save="handleSavePatient"
/>
<!-- Footer Actions -->
<div class="mt-6 flex justify-end gap-2 border-t border-t-slate-300 pt-4">
<Button
variant="outline"
type="button"
class="h-[40px] min-w-[120px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50 hover:text-orange-400"
@click="handleEvent('cancel')"
>
<Icon
name="i-lucide-x"
class="h-5 w-5"
/>
Batal
</Button>
<Button
type="button"
class="h-[40px] min-w-[120px] text-white"
:disabled="isSaveDisabled"
@click="handleSaveClick"
>
<Icon
name="i-lucide-save"
class="h-5 w-5"
/>
Simpan
</Button>
</div>
</template>
+167 -22
View File
@@ -1,14 +1,27 @@
<script setup lang="ts">
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
// Components
import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
import SummaryCard from '~/components/pub/my-ui/summary-card/summary-card.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import Filter from '~/components/pub/my-ui/nav-header/filter.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
// Types
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import { ActionEvents } from '~/components/pub/my-ui/data/types'
// Services
import { getList as getEncounterList, remove as removeEncounter } from '~/services/encounter.service'
// UI
import { toast } from '~/components/pub/ui/toast'
const props = defineProps<{
classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient'
subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
type: string
}>()
@@ -21,13 +34,27 @@ const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
const hreaderPrep: HeaderPrep = {
title: 'Kunjungan',
icon: 'i-lucide-users',
addNav: {
label: 'Tambah',
onClick: () => navigateTo('/rehab/encounter/add'),
onClick: () => {
if (props.classCode === 'ambulatory' && props.subClassCode === 'rehab') {
navigateTo('/rehab/encounter/add')
}
if (props.classCode === 'ambulatory' && props.subClassCode === 'reg') {
navigateTo('/outpatient/encounter/add')
}
if (props.classCode === 'emergency') {
navigateTo('/emergency/encounter/add')
}
if (props.classCode === 'inpatient') {
navigateTo('/inpatient/encounter/add')
}
},
},
}
@@ -47,40 +74,119 @@ const refSearchNav: RefSearchNav = {
// Loading state management
async function getPatientList() {
isLoading.isTableLoading = true
const resp = await xfetch('/api/v1/encounter?includes=patient,patient-person')
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
/**
* Get base path for encounter routes based on classCode and subClassCode
*/
function getBasePath(): string {
if (props.classCode === 'ambulatory' && props.subClassCode === 'rehab') {
return '/rehab/encounter'
}
isLoading.isTableLoading = false
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
}
onMounted(() => {
getPatientList()
})
async function getPatientList() {
isLoading.isTableLoading = true
try {
const params: any = { includes: 'patient,patient-person' }
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 || []
}
} catch (error) {
console.error('Error fetching encounter list:', error)
} finally {
isLoading.isTableLoading = false
}
}
// Handle confirmation result
async function handleConfirmDelete(record: any, action: string) {
if (action === 'delete' && record?.id) {
try {
const result = await removeEncounter(record.id)
if (result.success) {
toast({
title: 'Berhasil',
description: 'Kunjungan berhasil dihapus',
variant: 'default',
})
await getPatientList() // Refresh list
} else {
const errorMessage = result.body?.message || 'Gagal menghapus kunjungan'
toast({
title: 'Gagal',
description: errorMessage,
variant: 'destructive',
})
}
} catch (error: any) {
console.error('Error deleting encounter:', error)
toast({
title: 'Gagal',
description: error?.message || 'Gagal menghapus kunjungan',
variant: 'destructive',
})
} finally {
// Reset state
recId.value = 0
recAction.value = ''
recItem.value = null
isRecordConfirmationOpen.value = false
}
}
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
isRecordConfirmationOpen.value = false
}
watch(
() => recAction.value,
() => {
console.log('recAction.value', recAction.value)
if (recAction.value === ActionEvents.showConfirmDelete) {
isRecordConfirmationOpen.value = true
return
}
const basePath = getBasePath()
if (props.type === 'encounter') {
if (recAction.value === 'showDetail') {
navigateTo(`/rehab/encounter/${recId.value}/detail`)
navigateTo(`${basePath}/${recId.value}/detail`)
} else if (recAction.value === 'showEdit') {
navigateTo(`/rehab/encounter/${recId.value}/edit`)
navigateTo(`${basePath}/${recId.value}/edit`)
} else if (recAction.value === 'showProcess') {
navigateTo(`/rehab/encounter/${recId.value}/process`)
navigateTo(`${basePath}/${recId.value}/process`)
} else {
// handle other actions
}
} else if (props.type === 'registration') {
// Handle registration type if needed
if (recAction.value === 'showDetail') {
navigateTo(`/rehab/registration/${recId.value}/detail`)
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/detail`)
} else if (recAction.value === 'showEdit') {
navigateTo(`/rehab/registration/${recId.value}/edit`)
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/edit`)
} else if (recAction.value === 'showProcess') {
navigateTo(`/rehab/registration/${recId.value}/process`)
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/process`)
} else {
// handle other actions
}
@@ -92,6 +198,10 @@ provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
onMounted(() => {
getPatientList()
})
</script>
<template>
@@ -101,7 +211,10 @@ provide('table_data_loader', isLoading)
/>
<Separator class="my-4 xl:my-5" />
<Filter :ref-search-nav="refSearchNav" />
<Filter
:prep="hreaderPrep"
:ref-search-nav="refSearchNav"
/>
<AppEncounterList :data="data" />
@@ -111,6 +224,38 @@ provide('table_data_loader', isLoading)
size="lg"
prevent-outside
>
<AppEncounterFilter />
<AppEncounterFilter
:installation="{
msg: { placeholder: 'Pilih' },
items: [],
}"
:schema="{}"
/>
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="handleConfirmDelete"
@cancel="handleCancelConfirmation"
>
<template #default="{ record }">
<div class="text-sm">
<p>
<strong>ID:</strong>
{{ record?.id }}
</p>
<p v-if="record?.patient?.person?.name">
<strong>Pasien:</strong>
{{ record.patient.person.name }}
</p>
<p v-if="record?.class_code">
<strong>Kelas:</strong>
{{ record.class_code }}
</p>
</div>
</template>
</RecordConfirmation>
</template>
+20 -7
View File
@@ -11,10 +11,12 @@ import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
// PLASE ORDER BY TAB POSITION
import Status from '~/components/content/encounter/status.vue'
import AssesmentFunctionList from '~/components/content/assesment-function/list.vue'
import AssesmentFunctionList from '~/components/content/soapi/entry.vue'
import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
import PrescriptionList from '~/components/content/prescription/list.vue'
import Prescription from '~/components/content/prescription/main.vue'
import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
import Radiology from '~/components/content/radiology-order/main.vue'
import Consultation from '~/components/content/consultation/list.vue'
const route = useRoute()
@@ -38,21 +40,32 @@ const data = dataResBody?.data ?? null
const tabs: TabItem[] = [
{ value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } },
{ value: 'early-medical-assessment', label: 'Pengkajian Awal Medis', component: EarlyMedicalAssesmentList },
{
value: 'early-medical-assessment',
label: 'Pengkajian Awal Medis',
component: EarlyMedicalAssesmentList,
props: { encounter: data, type: 'early-medic', label: 'Pengkajian Awal Medis' },
},
{
value: 'rehab-medical-assessment',
label: 'Pengkajian Awal Medis Rehabilitasi Medis',
component: EarlyMedicalRehabList,
props: { encounter: data, type: 'early-rehab', label: 'Pengkajian Awal Medis Rehabilitasi Medis' },
},
{
value: 'function-assessment',
label: 'Asesmen Fungsi',
component: AssesmentFunctionList,
props: { encounter: data, type: 'function', label: 'Asesmen Fungsi' },
},
{ value: 'function-assessment', label: 'Asesmen Fungsi', component: AssesmentFunctionList },
{ value: 'therapy-protocol', label: 'Protokol Terapi' },
{ value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' },
{ value: 'consent', label: 'General Consent' },
{ value: 'patient-note', label: 'CPRJ' },
{ value: 'prescription', label: 'Order Obat', component: PrescriptionList },
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.id } },
{ value: 'device', label: 'Order Alkes' },
{ value: 'mcu-radiology', label: 'Order Radiologi' },
{ value: 'mcu-lab-pc', label: 'Order Lab PK' },
{ value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.id } },
{ value: 'mcu-lab-cp', label: 'Order Lab PK', component: CpLabOrder, props: { encounter_id: data.id } },
{ value: 'mcu-lab-micro', label: 'Order Lab Mikro' },
{ value: 'mcu-lab-pa', label: 'Order Lab PA' },
{ value: 'medical-action', label: 'Order Ruang Tindakan' },