Merge pull request #138 from dikstub-rssa/feat/fe-sep-113

Feat: Integration Vclaim Sep
This commit is contained in:
Munawwirul Jamal
2025-11-10 23:14:25 +07:00
committed by GitHub
68 changed files with 5483 additions and 1317 deletions

No files matched your search

+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>
+516 -77
View File
@@ -1,113 +1,552 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
// Components
import AppSepEntryForm from '~/components/app/sep/entry-form.vue'
import AppViewPatient from '~/components/app/patient/view-patient.vue'
import AppViewHistory from '~/components/app/sep/view-history.vue'
import AppViewLetter from '~/components/app/sep/view-letter.vue'
import { toast } from '~/components/pub/ui/toast'
// Types
import type { SepHistoryData } from '~/components/app/sep/list-cfg.history'
import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
// Constants
import {
serviceTypes,
serviceAssessments,
registerMethods,
trafficAccidents,
supportCodes,
procedureTypes,
purposeOfVisits,
classLevels,
classLevelUpgrades,
classPaySources,
} from '~/lib/constants.vclaim'
// Services
import {
getList as getSpecialistList,
getValueTreeItems as getSpecialistTreeItems,
} from '~/services/specialist.service'
import { getValueLabelList as getProvinceList } from '~/services/vclaim-region-province.service'
import { getValueLabelList as getCityList } from '~/services/vclaim-region-city.service'
import { getValueLabelList as getDistrictList } from '~/services/vclaim-region-district.service'
import { getValueLabelList as getDoctorLabelList } from '~/services/vclaim-doctor.service'
import { getValueLabelList as getHealthFacilityLabelList } from '~/services/vclaim-healthcare.service'
import { getValueLabelList as getDiagnoseLabelList } from '~/services/vclaim-diagnose.service'
import { getList as getMemberList } from '~/services/vclaim-member.service'
import { getList as getHospitalLetterList } from '~/services/vclaim-reference-hospital-letter.service'
import { getList as getControlLetterList } from '~/services/vclaim-control-letter.service'
import { getList as getMonitoringHistoryList } from '~/services/vclaim-monitoring-history.service'
import { create as createSep, makeSepData } from '~/services/vclaim-sep.service'
// Handlers
import {
patients,
selectedPatient,
selectedPatientObject,
paginationMeta,
getPatientsList,
getPatientCurrent,
getPatientByIdentifierSearch,
} from '~/handlers/patient.handler'
const route = useRoute()
const openPatient = ref(false)
const openLetter = ref(false)
const openHistory = ref(false)
const selectedPatient = ref('3456512345678880')
const selectedLetter = ref('SK22334442')
const selectedLetter = ref('')
const selectedObjects = ref<any>({})
const selectedServiceType = ref<string>('')
const selectedAdmissionType = ref<string>('')
const histories = ref<Array<SepHistoryData>>([])
const letters = ref<Array<any>>([])
const doctors = ref<Array<{ value: string | number; label: string }>>([])
const diagnoses = ref<Array<{ value: string | number; label: string }>>([])
const facilitiesFrom = ref<Array<{ value: string | number; label: string }>>([])
const facilitiesTo = ref<Array<{ value: string | number; label: string }>>([])
const supportCodesList = ref<Array<{ value: string; label: string }>>([])
const serviceTypesList = ref<Array<{ value: string; label: string }>>([])
const registerMethodsList = ref<Array<{ value: string; label: string }>>([])
const accidentsList = ref<Array<{ value: string; label: string }>>([])
const purposeOfVisitsList = ref<Array<{ value: string; label: string }>>([])
const proceduresList = ref<Array<{ value: string; label: string }>>([])
const assessmentsList = ref<Array<{ value: string; label: string }>>([])
const provincesList = ref<Array<{ value: string; label: string }>>([])
const citiesList = ref<Array<{ value: string; label: string }>>([])
const districtsList = ref<Array<{ value: string; label: string }>>([])
const classLevelsList = ref<Array<{ value: string; label: string }>>([])
const classLevelUpgradesList = ref<Array<{ value: string; label: string }>>([])
const classPaySourcesList = ref<Array<{ value: string; label: string }>>([])
const isServiceHidden = ref(false)
const isSaveLoading = ref(false)
const isLetterReadonly = ref(false)
const specialistsTree = ref<TreeItem[]>([])
const resourceType = ref('')
const resourcePath = ref('')
const patients = [
{
ktp: '3456512345678880',
rm: 'RM23311224',
bpjs: '334423213214',
nama: 'Ahmad Baidowi',
},
{
ktp: '345678804565123',
rm: 'RM23455667',
bpjs: '33442367656',
nama: 'Bian Maulana',
},
]
async function getMonitoringHistoryMappers() {
histories.value = []
const dateFirst = new Date()
const dateLast = new Date()
dateLast.setMonth(dateFirst.getMonth() - 3)
const cardNumber =
selectedPatientObject.value?.person?.residentIdentityNumber || selectedPatientObject.value?.number || ''
const result = await getMonitoringHistoryList({
cardNumber: cardNumber,
startDate: dateFirst.toISOString().substring(0, 10),
endDate: dateLast.toISOString().substring(0, 10),
})
if (result && result.success && result.body) {
const historiesRaw = result.body?.response?.histori || []
if (!historiesRaw) return
historiesRaw.forEach((result: any) => {
histories.value.push({
sepNumber: result.noSep,
sepDate: result.tglSep,
referralNumber: result.noRujukan,
diagnosis:
result.diagnosa && typeof result.diagnosa === 'string' && result.diagnosa.length > 20
? result.diagnosa.toString().substring(0, 17) + '...'
: '-',
serviceType: !result.jnsPelayanan ? '-' : result.jnsPelayanan === '1' ? 'Rawat Jalan' : 'Rawat Inap',
careClass: result.kelasRawat,
})
})
}
}
const letters = [
{
noSurat: 'SK22334442',
tglRencana: '12 Agustus 2025',
noSep: 'SEP3232332',
namaPasien: 'Ahmad Baidowi',
noBpjs: '33442331214',
klinik: 'Penyakit Dalam',
dokter: 'dr. Andi Prasetyo, Sp.PD-KHOM',
},
{
noSurat: 'SK99120039',
tglRencana: '12 Agustus 2025',
noSep: 'SEP4443232',
namaPasien: 'Bian Maulana',
noBpjs: '33442367656',
klinik: 'Gigi',
dokter: 'dr. Achmad Suparjo',
},
]
async function getLetterMappers(admissionType: string, search: string) {
letters.value = []
let result = null
if (admissionType !== '3') {
result = await getHospitalLetterList({
letterNumber: search,
})
} else {
result = await getControlLetterList({
letterNumber: search,
mode: 'by-control',
})
if (result && result.success && result.body) {
const lettersRaw = result.body?.response || null
if (!lettersRaw) {
result = await getControlLetterList({
letterNumber: search,
mode: 'by-card',
})
}
}
if (result && result.success && result.body) {
const lettersRaw = result.body?.response || null
if (!lettersRaw) {
result = await getControlLetterList({
letterNumber: search,
mode: 'by-sep',
})
}
}
}
if (result && result.success && result.body) {
const lettersRaw = result.body?.response || null
if (!lettersRaw) return
if (admissionType === '3') {
letters.value = [
{
letterNumber: lettersRaw.noSuratKontrol || '',
plannedDate: lettersRaw.tglRencanaKontrol || '',
sepNumber: lettersRaw.sep.noSep || '',
patientName: lettersRaw.sep.peserta.nama || '',
bpjsCardNo: lettersRaw.sep.peserta.noKartu,
clinic: lettersRaw.sep.poli || '',
doctor: lettersRaw.sep.namaDokter || '',
},
]
} else {
// integrate ke sep ---
// "rujukan": {
// "noRujukan": "0212R0300625B000006", // rujukan?.noKunjungan
// "ppkRujukan": "0212R030",
// "tglRujukan": "2025-06-26",
// "asalRujukan": "2" // asalFaskes
// },
// "jnsPelayanan": "2",
// "ppkPelayanan": "1323R001",
// "poli": {
// "tujuan": "URO", // rujukan?.poliRujukan?.kode
// },
// "klsRawat": {
// "pembiayaan": "",
// "klsRawatHak": "2", // peserta.hakKelas?.kode
// "klsRawatNaik": "",
// "penanggungJawab": ""
// },
const histories = [
{
no_sep: "SP23311224",
tgl_sep: "12 Agustus 2025",
no_rujukan: "123444",
diagnosis: "C34.9 Karsinoma Paru",
pelayanan: "Rawat Jalan",
kelas: "Kelas II",
},
{
no_sep: "SP23455667",
tgl_sep: "11 Agustus 2025",
no_rujukan: "2331221",
diagnosis: "K35 Apendisitis akut",
pelayanan: "Rawat Jalan",
kelas: "Kelas II",
},
]
letters.value = [
{
letterNumber: lettersRaw?.rujukan?.noKunjungan || '',
plannedDate: lettersRaw?.rujukan?.tglKunjungan || '',
sepNumber: lettersRaw?.rujukan?.informasi?.eSEP || '-',
patientName: lettersRaw?.rujukan?.peserta.nama || '',
bpjsCardNo: lettersRaw?.rujukan?.peserta.noKartu || '',
clinic: lettersRaw?.rujukan?.poliRujukan.nama || '',
doctor: '',
information: {
facility: lettersRaw?.asalFaskes || '',
diagnoses: lettersRaw?.rujukan?.diagnosa?.kode || '',
serviceType: lettersRaw?.rujukan?.pelayanan?.kode || '',
classLevel: lettersRaw?.rujukan?.peserta?.hakKelas?.kode || '',
poly: lettersRaw?.rujukan?.poliRujukan?.kode || '',
cardNumber: lettersRaw?.rujukan?.peserta?.noKartu || '',
patientName: lettersRaw?.rujukan?.peserta?.nama || '',
patientPhone: lettersRaw?.rujukan?.peserta?.mr?.noTelepon || '',
medicalRecordNumber: lettersRaw?.rujukan?.peserta?.mr?.noMR || '',
},
},
]
}
}
}
function handleSavePatient() {
console.log('Pasien dipilih:', selectedPatient.value)
async function getPatientInternalMappers(id: string) {
try {
await getPatientCurrent(id)
if (selectedPatientObject.value) {
const patient = selectedPatientObject.value
selectedObjects.value['cardNumber'] = '-'
selectedObjects.value['nationalIdentity'] = patient?.person?.residentIdentityNumber || '-'
selectedObjects.value['medicalRecordNumber'] = patient?.number || '-'
selectedObjects.value['patientName'] = patient?.person?.name || '-'
selectedObjects.value['phoneNumber'] = patient?.person?.contacts?.[0]?.value || '-'
}
} catch (err) {
console.error('Failed to load patient from query params:', err)
}
}
async function getPatientExternalMappers(id: string, type: string) {
try {
const result = await getMemberList({
mode: type,
number: id,
date: new Date().toISOString().substring(0, 10),
})
if (result && result.success && result.body) {
const memberRaws = result.body?.response || null
selectedObjects.value['cardNumber'] = memberRaws?.peserta?.noKartu || ''
selectedObjects.value['nationalIdentity'] = memberRaws?.peserta?.nik || ''
selectedObjects.value['medicalRecordNumber'] = memberRaws?.peserta?.mr?.noMR || ''
selectedObjects.value['patientName'] = memberRaws?.peserta?.nama || ''
selectedObjects.value['phoneNumber'] = memberRaws?.peserta?.mr?.noTelepon || ''
selectedObjects.value['classLevel'] = memberRaws?.peserta?.hakKelas?.kode || ''
selectedObjects.value['status'] = memberRaws?.statusPeserta?.kode || ''
}
} catch (err) {
console.error('Failed to load patient from query params:', err)
}
}
function handleSaveLetter() {
console.log('Letter dipilih:', selectedLetter.value)
// Find the selected letter and get its plannedDate
const selectedLetterData = letters.value.find((letter) => letter.letterNumber === selectedLetter.value)
if (selectedLetterData && selectedLetterData.plannedDate) {
selectedObjects.value['letterDate'] = selectedLetterData.plannedDate
}
}
function handleEvent(value: string) {
if (value === 'search-patient') {
openPatient.value = true
async function handleSavePatient() {
selectedPatientObject.value = null
await getPatientInternalMappers(selectedPatient.value)
}
async function handleEvent(menu: string, value: any) {
if (menu === 'admission-type') {
selectedAdmissionType.value = value
return
}
if (value === 'search-letter') {
if (menu === 'service-type') {
selectedServiceType.value = value
doctors.value = await getDoctorLabelList({
serviceType: selectedServiceType.value || 1,
serviceDate: new Date().toISOString().substring(0, 10),
specialistCode: 0,
})
}
if (menu === 'search-patient') {
getPatientsList({ 'page-size': 10, includes: 'person' }).then(() => {
openPatient.value = true
})
return
}
if (menu === 'search-patient-by-identifier') {
const text = value.text
const type = value.type
const prevCardNumber = selectedPatientObject.value?.person?.residentIdentityNumber || ''
if (type === 'indentity' && text !== prevCardNumber) {
await getPatientByIdentifierSearch(text)
await getPatientExternalMappers(text, 'by-identity')
}
if (type === 'cardNumber' && text !== prevCardNumber) {
await getPatientExternalMappers(text, 'by-card')
}
return
}
if (menu === 'search-letter') {
isLetterReadonly.value = false
getLetterMappers(value.admissionType, value.search).then(() => {
if (letters.value.length > 0) {
const copyObjects = { ...selectedObjects.value }
selectedObjects.value = {}
selectedLetter.value = letters.value[0].letterNumber
isLetterReadonly.value = true
setTimeout(() => {
selectedObjects.value = copyObjects
selectedObjects.value['letterDate'] = letters.value[0].plannedDate
}, 100)
}
})
return
}
if (menu === 'open-letter') {
openLetter.value = true
return
}
if (value === 'history-sep') {
openHistory.value = true
if (menu === 'history-sep') {
getMonitoringHistoryMappers().then(() => {
openHistory.value = true
})
return
}
if (value === 'back') {
navigateTo('/bpjs/sep')
if (menu === 'back') {
navigateTo('/integration/bpjs/sep')
}
if (menu === 'save-sep') {
isSaveLoading.value = true
// value.destinationClinic = value.destinationClinic || ''
createSep(makeSepData(value))
.then((res) => {
const body = res?.body
const code = body?.metaData?.code
const message = body?.metaData?.message
if (code && code !== '200') {
toast({ title: 'Gagal', description: message || 'Gagal membuat SEP', variant: 'destructive' })
return
}
toast({ title: 'Berhasil', description: 'SEP berhasil dibuat', variant: 'default' })
if (resourceType.value === 'encounter') {
navigateTo(resourcePath.value)
return
}
navigateTo('/integration/bpjs/sep')
})
.catch((err) => {
console.error('Failed to save SEP:', err)
toast({ title: 'Gagal', description: err?.message || 'Gagal membuat SEP', variant: 'destructive' })
})
.finally(() => {
isSaveLoading.value = false
})
}
}
async function handleFetch(params: any) {
const menu = params.menu || ''
const value = params.value || ''
if (menu === 'diagnosis') {
diagnoses.value = await getDiagnoseLabelList({ diagnosa: value })
}
if (menu === 'clinic-from') {
facilitiesFrom.value = await getHealthFacilityLabelList({
healthcare: value,
healthcareType: selectedServiceType.value || 1,
})
}
if (menu === 'clinic-to') {
facilitiesTo.value = await getHealthFacilityLabelList({
healthcare: value,
healthcareType: selectedServiceType.value || 1,
})
}
if (menu === 'province') {
citiesList.value = await getCityList({ province: value })
districtsList.value = []
}
if (menu === 'city') {
districtsList.value = await getDistrictList({ city: value })
}
}
async function handleFetchSpecialists() {
try {
const specialistsResult = await getSpecialistList({ 'page-size': 100, includes: 'subspecialists' })
if (specialistsResult.success) {
const specialists = specialistsResult.body?.data || []
specialistsTree.value = getSpecialistTreeItems(specialists)
}
} catch (error) {
console.error('Error fetching specialist-subspecialist tree:', error)
}
}
async function handleInit() {
const facilities = await getHealthFacilityLabelList({
healthcare: 'Puskesmas',
healthcareType: selectedLetter.value || 1,
})
diagnoses.value = await getDiagnoseLabelList({ diagnosa: 'paru' })
facilitiesFrom.value = facilities
facilitiesTo.value = facilities
doctors.value = await getDoctorLabelList({
serviceType: selectedServiceType.value || 1,
serviceDate: new Date().toISOString().substring(0, 10),
specialistCode: 0,
})
provincesList.value = await getProvinceList()
serviceTypesList.value = Object.keys(serviceTypes).map((item) => ({
value: item.toString(),
label: serviceTypes[item],
})) as any
registerMethodsList.value = Object.keys(registerMethods)
.filter((item) => ![''].includes(item))
.map((item) => ({
value: item.toString(),
label: registerMethods[item],
})) as any
accidentsList.value = Object.keys(trafficAccidents).map((item) => ({
value: item.toString(),
label: trafficAccidents[item],
})) as any
purposeOfVisitsList.value = Object.keys(purposeOfVisits).map((item) => ({
value: item.toString(),
label: purposeOfVisits[item],
})) as any
proceduresList.value = Object.keys(procedureTypes).map((item) => ({
value: item.toString(),
label: procedureTypes[item],
})) as any
assessmentsList.value = Object.keys(serviceAssessments).map((item) => ({
value: item.toString(),
label: `${item.toString()} - ${serviceAssessments[item]}`,
})) as any
supportCodesList.value = Object.keys(supportCodes).map((item) => ({
value: item.toString(),
label: `${item.toString()} - ${supportCodes[item]}`,
})) as any
classLevelsList.value = Object.keys(classLevels).map((item) => ({
value: item.toString(),
label: classLevels[item],
})) as any
classLevelUpgradesList.value = Object.keys(classLevelUpgrades).map((item) => ({
value: item.toString(),
label: classLevelUpgrades[item],
})) as any
classPaySourcesList.value = Object.keys(classPaySources).map((item) => ({
value: item.toString(),
label: classPaySources[item],
})) as any
await handleFetchSpecialists()
if (route.query) {
const queries = route.query as any
isServiceHidden.value = queries['is-service'] === 'true'
selectedObjects.value = {}
if (queries['resource']) resourceType.value = queries['resource']
if (queries['resource-path']) resourcePath.value = queries['resource-path']
if (queries['doctor-code']) selectedObjects.value['doctorCode'] = queries['doctor-code']
if (queries['specialist-code']) selectedObjects.value['subSpecialistCode'] = queries['specialist-code']
if (queries['sub-specialist-code']) selectedObjects.value['subSpecialistCode'] = queries['sub-specialist-code']
if (queries['card-number']) selectedObjects.value['cardNumber'] = queries['card-number']
if (queries['register-date']) selectedObjects.value['registerDate'] = queries['register-date']
if (queries['sep-type']) selectedObjects.value['sepType'] = queries['sep-type']
if (queries['sep-number']) selectedObjects.value['sepNumber'] = queries['sep-number']
if (queries['register-date']) selectedObjects.value['registerDate'] = queries['register-date']
if (queries['payment-type']) selectedObjects.value['paymentType'] = queries['payment-type']
if (queries['patient-id']) {
await getPatientInternalMappers(queries['patient-id'])
}
if (queries['card-number']) {
const resultMember = await getMemberList({
mode: 'by-card',
number: queries['card-number'],
date: new Date().toISOString().substring(0, 10),
})
console.log(resultMember)
}
delete selectedObjects.value['is-service']
}
}
onMounted(async () => {
await handleInit()
})
</script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<Icon name="i-lucide-panel-bottom" class="me-2" />
<span class="font-semibold">Tambah</span> SEP
<Icon
name="i-lucide-panel-bottom"
class="me-2"
/>
<span class="font-semibold">Tambah</span>
SEP
</div>
<AppSepEntryForm @event="handleEvent" />
<AppSepTableSearchPatient
<AppSepEntryForm
:is-save-loading="isSaveLoading"
:is-service="isServiceHidden"
:doctors="doctors"
:diagnoses="diagnoses"
:facilities-from="facilitiesFrom"
:facilities-to="facilitiesTo"
:service-types="serviceTypesList"
:register-methods="registerMethodsList"
:accidents="accidentsList"
:purposes="purposeOfVisitsList"
:procedures="proceduresList"
:assessments="assessmentsList"
:support-codes="supportCodesList"
:provinces="provincesList"
:cities="citiesList"
:districts="districtsList"
:class-levels="classLevelsList"
:class-level-upgrades="classLevelUpgradesList"
:class-pay-sources="classPaySourcesList"
:specialists="specialistsTree"
:objects="selectedObjects"
@fetch="handleFetch"
@event="handleEvent"
/>
<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"
/>
<AppSepTableSearchLetter
v-model:open="openLetter"
v-model:selected="selectedLetter"
:letters="letters"
@save="handleSaveLetter"
/>
<AppSepTableHistorySep
<AppViewHistory
v-model:open="openHistory"
:histories="histories"
/>
<AppViewLetter
v-model:open="openLetter"
:letters="letters"
:menu="selectedAdmissionType !== '3' ? 'control' : 'reference'"
:selected="selectedLetter"
:pagination-meta="{ recordCount: 0, page: 1, pageSize: 10, totalPage: 0 } as any"
@fetch="(value) => getLetterMappers(value.admissionType, value.search)"
@save="handleSaveLetter"
/>
</template>
+107 -84
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
// Components
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
@@ -9,10 +10,19 @@ import {
DropdownMenuContent,
DropdownMenuItem,
} from '~/components/pub/ui/dropdown-menu'
import AppSepList from '~/components/app/sep/list.vue'
// Icons
import { X, Check } from 'lucide-vue-next'
// Types
import type { VclaimSepData } from '~/models/vclaim'
// Services
import { getList as geMonitoringVisitList } from '~/services/vclaim-monitoring-visit.service'
const search = ref('')
const dateRange = ref('12 Agustus 2025 - 32 Agustus 2025')
const dateRange = ref('12 Agustus 2025 - 31 Agustus 2025')
const open = ref(false)
const sepData = {
@@ -21,21 +31,6 @@ const sepData = {
nama: 'Kenzie',
}
interface SepData {
tgl_sep: string
no_sep: string
pelayanan: string
jalur: string
no_rm: string
nama_pasien: string
no_kartu_bpjs: string
no_surat_kontrol: string
tgl_surat_kontrol: string
klinik_tujuan: string
dpjp: string
diagnosis_awal: string
}
const paginationMeta = reactive<PaginationMeta>({
recordCount: 0,
page: 1,
@@ -49,53 +44,7 @@ function handlePageChange(page: number) {
console.log('pageChange', page)
}
const data = ref<SepData[]>([])
// contoh data dummy
const rows = [
{
tgl_sep: '12 Agustus 2025',
no_sep: 'SP23311224',
pelayanan: 'Rawat Jalan',
jalur: 'Kontrol',
no_rm: 'RM23311224',
nama_pasien: 'Ahmad Baidowi',
no_kartu_bpjs: '334423231214',
no_surat_kontrol: 'SK22334442',
tgl_surat_kontrol: '13 Agustus 2024',
klinik_tujuan: 'Penyakit dalam',
dpjp: 'dr. Andi Prasetyo, Sp.PD-KHOM',
diagnosis_awal: 'C34.9 - Karsinoma Paru',
},
{
tgl_sep: '12 Agustus 2025',
no_sep: 'SP23311224',
pelayanan: 'Rawat Jalan',
jalur: 'Kontrol',
no_rm: 'RM001234',
nama_pasien: 'Kenzie',
no_kartu_bpjs: '12301234',
no_surat_kontrol: '123456',
tgl_surat_kontrol: '10 Agustus 2024',
klinik_tujuan: 'Penyakit dalam',
dpjp: 'Dr. Andreas Sutaji',
diagnosis_awal: 'Bronchitis',
},
{
tgl_sep: '11 Agustus 2025',
no_sep: 'SP23455667',
pelayanan: 'Rawat Jalan',
jalur: 'Kontrol',
no_rm: 'RM001009',
nama_pasien: 'Abraham Sulaiman',
no_kartu_bpjs: '334235',
no_surat_kontrol: '123334',
tgl_surat_kontrol: '11 Agustus 2024',
klinik_tujuan: 'Penyakit dalam',
dpjp: 'Dr. Andreas Sutaji',
diagnosis_awal: 'Paru-paru basah',
},
]
const data = ref<VclaimSepData[]>([])
const refSearchNav: RefSearchNav = {
onClick: () => {
@@ -123,18 +72,64 @@ const headerPrep: HeaderPrep = {
addNav: {
label: 'Tambah',
onClick: () => {
navigateTo('/bpjs/sep/add')
navigateTo('/integration/bpjs/sep/add')
},
},
}
async function getSepList() {
async function getMonitoringVisitMappers() {
isLoading.dataListLoading = true
data.value = [...rows]
data.value = []
const dateFirst = new Date()
const result = await geMonitoringVisitList({
date: dateFirst.toISOString().substring(0, 10),
serviceType: 1,
})
if (result && result.success && result.body) {
const visitsRaw = result.body?.response?.sep || []
if (!visitsRaw) {
isLoading.dataListLoading = false
return
}
visitsRaw.forEach((result: any) => {
// Format pelayanan: "R.Inap" -> "Rawat Inap", "1" -> "Rawat Jalan", dll
let serviceType = result.jnsPelayanan || '-'
if (serviceType === 'R.Inap') {
serviceType = 'Rawat Inap'
} else if (serviceType === '1' || serviceType === 'R.Jalan') {
serviceType = 'Rawat Jalan'
}
data.value.push({
letterDate: result.tglSep || '-',
letterNumber: result.noSep || '-',
serviceType: serviceType,
flow: '-',
medicalRecordNumber: '-',
patientName: result.nama || '-',
cardNumber: result.noKartu || '-',
controlLetterNumber: result.noRujukan || '-',
controlLetterDate: result.tglPlgSep || '-',
clinicDestination: result.poli || '-',
attendingDoctor: '-',
diagnosis: result.diagnosa || '-',
careClass: result.kelasRawat || '-',
})
})
}
isLoading.dataListLoading = false
}
function exportCsv() {
async function getSepList() {
await getMonitoringVisitMappers()
}
function exportCsv() {
console.log('Ekspor CSV dipilih')
// tambahkan logic untuk generate CSV
}
@@ -169,19 +164,29 @@ provide('table_data_loader', isLoading)
</script>
<template>
<div class="rounded-md border p-4">
<div class="p-4">
<Header :prep="{ ...headerPrep }" />
<!-- Filter Bar -->
<div class="my-2 flex flex-wrap items-center gap-2">
<!-- Search -->
<Input v-model="search" placeholder="Cari No. SEP / No. Kartu BPJS..." class="w-72" />
<Input
v-model="search"
placeholder="Cari No. SEP / No. Kartu BPJS..."
class="w-72"
/>
<!-- Date Range -->
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" class="h-[40px] w-72 border-gray-400 bg-white text-right font-normal">
<Button
variant="outline"
class="h-[40px] w-72 border-gray-400 bg-white text-right font-normal"
>
{{ dateRange }}
<Icon name="i-lucide-calendar" class="h-5 w-5" />
<Icon
name="i-lucide-calendar"
class="h-5 w-5"
/>
</Button>
</PopoverTrigger>
<PopoverContent class="p-2">
@@ -196,24 +201,34 @@ provide('table_data_loader', isLoading)
variant="outline"
class="ml-auto h-[40px] w-[120px] rounded-md border-green-600 text-green-600 hover:bg-green-50"
>
<Icon name="i-lucide-download" class="h-5 w-5" /> Ekspor
<Icon
name="i-lucide-download"
class="h-5 w-5"
/>
Ekspor
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-40">
<DropdownMenuItem @click="exportCsv"> Ekspor CSV </DropdownMenuItem>
<DropdownMenuItem @click="exportExcel"> Ekspor Excel </DropdownMenuItem>
<DropdownMenuItem @click="exportCsv">Ekspor CSV</DropdownMenuItem>
<DropdownMenuItem @click="exportExcel">Ekspor Excel</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="rounded-md border p-4">
<AppSepList v-if="!isLoading.dataListLoading" :data="data" />
<div class="mt-4">
<AppSepList
v-if="!isLoading.dataListLoading"
:data="data"
/>
</div>
<!-- Pagination -->
<template v-if="paginationMeta">
<div v-if="paginationMeta.totalPage > 1">
<PubMyUiPagination :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<PubMyUiPagination
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
</template>
@@ -226,9 +241,7 @@ provide('table_data_loader', isLoading)
<DialogTitle>Hapus SEP</DialogTitle>
</DialogHeader>
<DialogDescription class="text-gray-700">
Apakah anda yakin ingin menghapus SEP dengan data:
</DialogDescription>
<DialogDescription class="text-gray-700">Apakah anda yakin ingin menghapus SEP dengan data:</DialogDescription>
<div class="mt-4 space-y-2 text-sm">
<p>No. SEP : {{ sepData.no_sep }}</p>
@@ -237,11 +250,21 @@ provide('table_data_loader', isLoading)
</div>
<DialogFooter class="mt-6 flex justify-end gap-3">
<Button variant="outline" class="border-green-600 text-green-600 hover:bg-green-50" @click="open = false">
<X class="mr-1 h-4 w-4" /> Tidak
<Button
variant="outline"
class="border-green-600 text-green-600 hover:bg-green-50"
@click="open = false"
>
<X class="mr-1 h-4 w-4" />
Tidak
</Button>
<Button variant="destructive" class="bg-red-600 hover:bg-red-700" @click="handleDelete">
<Check class="mr-1 h-4 w-4" /> Ya
<Button
variant="destructive"
class="bg-red-600 hover:bg-red-700"
@click="handleDelete"
>
<Check class="mr-1 h-4 w-4" />
Ya
</Button>
</DialogFooter>
</DialogContent>