diff --git a/app/components/app/encounter/dropdown-action.vue b/app/components/app/encounter/dropdown-action.vue new file mode 100644 index 00000000..bd7a4974 --- /dev/null +++ b/app/components/app/encounter/dropdown-action.vue @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + {{ item.label }} + + + + + + diff --git a/app/components/app/encounter/entry-form.vue b/app/components/app/encounter/entry-form.vue index 9c24a0c6..22acbbb8 100644 --- a/app/components/app/encounter/entry-form.vue +++ b/app/components/app/encounter/entry-form.vue @@ -20,6 +20,7 @@ import type { TreeItem } from '~/components/pub/my-ui/select-tree/type' // Helpers import { toTypedSchema } from '@vee-validate/zod' import { useForm } from 'vee-validate' +import { refDebounced } from '@vueuse/core' const props = defineProps<{ isLoading?: boolean @@ -92,8 +93,9 @@ watch(subSpecialistId, async (newValue) => { } }) -// Watch SEP number changes to notify parent -watch(sepNumber, (newValue) => { +// Debounced SEP number watcher: emit change only after user stops typing +const debouncedSepNumber = refDebounced(sepNumber, 500) +watch(debouncedSepNumber, (newValue) => { emit('event', 'sep-number-changed', newValue) }) @@ -452,7 +454,11 @@ defineExpose({ name="i-lucide-loader-2" class="h-4 w-4 animate-spin" /> - + + +// Components +import { Button } from '~/components/pub/ui/button' + +const route = useRoute() +const router = useRouter() + +// Track active menu item from query param +const activeMenu = computed(() => route.query.menu as string || '') + +interface ButtonItems { + label: string + icon: string + value: string + type: 'icon' | 'image' +} + +const itemsOne: ButtonItems[] = [ + { + label: 'Data Pendaftaran', + icon: 'i-lucide-file', + value: 'register', + type: 'icon', + }, + { + label: 'Status Pembayaran', + icon: 'i-lucide-banknote-arrow-down', + value: 'status', + type: 'icon', + }, + { + label: 'Riwayat Pasien', + icon: 'i-lucide-history', + value: 'history', + type: 'icon', + }, + { + label: 'Penunjang', + icon: 'i-lucide-library-big', + value: 'support', + type: 'icon', + }, + { + label: 'Resep', + icon: 'i-lucide-pill', + value: 'receipt', + type: 'icon', + }, + { + label: 'DPJP', + icon: 'i-lucide-stethoscope', + value: 'doctor', + type: 'icon', + }, + { + label: 'I-Care BPJS', + icon: '/bpjs.png', + value: 'bpjs', + type: 'image', + }, + { + label: 'File SEP', + icon: 'i-lucide-file', + value: 'sep', + type: 'icon', + }, +] + +const itemsTwo: ButtonItems[] = [ + { + label: 'Tarif Tindakan', + icon: 'i-lucide-banknote-arrow-down', + value: 'price-list', + type: 'icon', + }, + { + label: 'Tarif Tindakan Paket', + icon: 'i-lucide-banknote-arrow-down', + value: 'price-list-package', + type: 'icon', + }, +] + +function handleClick(value: string) { + router.replace({ path: route.path, query: { menu: value } }) +} + + + + + History Pasien: + + + + + {{ item.label }} + + + Billing Pasien: + + + + {{ item.label }} + + + + diff --git a/app/components/app/encounter/list.cfg.ts b/app/components/app/encounter/list.cfg.ts index 9ebbc34f..cc0b951c 100644 --- a/app/components/app/encounter/list.cfg.ts +++ b/app/components/app/encounter/list.cfg.ts @@ -6,7 +6,7 @@ import { getAge } from '~/lib/date' type SmallDetailDto = Encounter -const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-pdud.vue')) +const action = defineAsyncComponent(() => import('./dropdown-action.vue')) const statusBadge = defineAsyncComponent(() => import('./status-badge.vue')) export const config: Config = { diff --git a/app/components/app/encounter/list.vue b/app/components/app/encounter/list.vue index 1d58d6a7..f1c5bde6 100644 --- a/app/components/app/encounter/list.vue +++ b/app/components/app/encounter/list.vue @@ -2,13 +2,13 @@ import { config } from './list.cfg' const props = defineProps<{ - data: any[] + data: any[], }>() diff --git a/app/components/app/encounter/patient-info-collapsible.vue b/app/components/app/encounter/patient-info-collapsible.vue new file mode 100644 index 00000000..924a8c2e --- /dev/null +++ b/app/components/app/encounter/patient-info-collapsible.vue @@ -0,0 +1,28 @@ + + + + + + + + + Data Pasien: + + + + + + + + diff --git a/app/components/app/encounter/patient-info.vue b/app/components/app/encounter/patient-info.vue new file mode 100644 index 00000000..ea41199e --- /dev/null +++ b/app/components/app/encounter/patient-info.vue @@ -0,0 +1,18 @@ + + + + + Data Pasien: + + + diff --git a/app/components/app/encounter/quick-info-full.vue b/app/components/app/encounter/quick-info-full.vue new file mode 100644 index 00000000..4cbc5bfb --- /dev/null +++ b/app/components/app/encounter/quick-info-full.vue @@ -0,0 +1,247 @@ + + + + Data Pasien: + + + + + No. RM + : + + {{ data.patient.number || '-' }} + + + + + + Tgl. Lahir + : + + {{ birthDateFormatted }} + + + + + + Cara Bayar + : + + {{ paymentTypeLabel }} + + + + + + No Bed + : + + {{ bedNumber }} + + + + + + No. Bed + : + + {{ data.patient.person.name || '-' }} + + + + + + Tgl. Masuk RS + : + + {{ registeredDateFormatted }} + + + + + + No. Billing + : + + {{ billingNumber }} + + + + + + DPJP + : + + {{ dpjp }} + + + + + + Alamat + : + + {{ address }} + + + + + + Jns. Kelamin + : + + {{ genderLabel }} + + + + + + Nama Ruang + : + + {{ roomName }} + + + + diff --git a/app/components/app/encounter/quick-shortcut.vue b/app/components/app/encounter/quick-shortcut.vue new file mode 100644 index 00000000..751b2feb --- /dev/null +++ b/app/components/app/encounter/quick-shortcut.vue @@ -0,0 +1,130 @@ + + + + Menu Cepat: + + + + + {{ item.label }} + + + + + + {{ item.label }} + + + diff --git a/app/components/app/encounter/status-badge.vue b/app/components/app/encounter/status-badge.vue index c8d4cd06..2ddd6549 100644 --- a/app/components/app/encounter/status-badge.vue +++ b/app/components/app/encounter/status-badge.vue @@ -12,7 +12,7 @@ const statusCodeColors: Record = { review: 'fresh', process: 'fresh', done: 'positive', - canceled: 'destructive', + cancel: 'destructive', rejected: 'destructive', skiped: 'negative', } diff --git a/app/components/app/sep/entry-form.vue b/app/components/app/sep/entry-form.vue index 35956ad7..0aa33178 100644 --- a/app/components/app/sep/entry-form.vue +++ b/app/components/app/sep/entry-form.vue @@ -143,12 +143,16 @@ watch(props, (value) => { nationalId.value = objects?.nationalIdentity || '-' medicalRecordNumber.value = objects?.medicalRecordNumber || '-' patientName.value = objects?.patientName || '-' + phoneNumber.value = objects?.phoneNumber || '-' if (objects?.sepType === 'internal') { admissionType.value = '4' } if (objects?.sepType === 'external') { admissionType.value = '1' } + if (objects?.diagnoseLabel) { + initialDiagnosis.value = objects?.diagnoseLabel + } isDateReload.value = true setTimeout(() => { if (objects?.letterDate) { @@ -176,6 +180,9 @@ onMounted(() => { if (!isService.value) { serviceType.value = '2' } + if (!admissionType.value) { + admissionType.value = '1' + } }) diff --git a/app/components/app/sep/list.vue b/app/components/app/sep/list.vue index 2afaa2cc..78a0b740 100644 --- a/app/components/app/sep/list.vue +++ b/app/components/app/sep/list.vue @@ -11,7 +11,8 @@ const props = defineProps<{ diff --git a/app/components/content/chemotherapy/process.vue b/app/components/content/chemotherapy/process.vue index 7f355b4b..2a61c468 100644 --- a/app/components/content/chemotherapy/process.vue +++ b/app/components/content/chemotherapy/process.vue @@ -3,5 +3,5 @@ import EncounterHome from '~/components/content/encounter/home.vue' - + diff --git a/app/components/content/encounter/detail.vue b/app/components/content/encounter/detail.vue new file mode 100644 index 00000000..5f2255c5 --- /dev/null +++ b/app/components/content/encounter/detail.vue @@ -0,0 +1,43 @@ + + + + + + + + + + diff --git a/app/components/content/encounter/entry.vue b/app/components/content/encounter/entry.vue index 78297754..228d165e 100644 --- a/app/components/content/encounter/entry.vue +++ b/app/components/content/encounter/entry.vue @@ -1,42 +1,14 @@ - - - - - - - - - - diff --git a/app/components/content/encounter/list.vue b/app/components/content/encounter/list.vue index 655a553d..5bf8318c 100644 --- a/app/components/content/encounter/list.vue +++ b/app/components/content/encounter/list.vue @@ -6,6 +6,10 @@ 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' +import { useSidebar } from '~/components/pub/ui/sidebar/utils' + +// Libs +import { getPositionAs } from '~/lib/roles' // Types import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type' @@ -14,11 +18,18 @@ 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' +import { getList as getEncounterList, remove as removeEncounter, cancel as cancelEncounter } from '~/services/encounter.service' // UI import { toast } from '~/components/pub/ui/toast' + +const { setOpen } = useSidebar() +setOpen(true) + +const { getActiveRole } = useUserStore() +const activeRole = getActiveRole() +const activePosition = ref(getPositionAs(activeRole)) const props = defineProps<{ classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient' subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk' @@ -35,6 +46,7 @@ const recAction = ref('') const recItem = ref(null) const isFormEntryDialogOpen = ref(false) const isRecordConfirmationOpen = ref(false) +const isRecordCancelOpen = ref(false) const hreaderPrep: HeaderPrep = { title: 'Kunjungan', @@ -114,6 +126,43 @@ async function getPatientList() { } } +// Handle confirmation result +async function handleConfirmCancel(record: any, action: string) { + if (action === 'deactivate' && record?.id) { + try { + const result = await cancelEncounter(record.id) + if (result.success) { + toast({ + title: 'Berhasil', + description: 'Kunjungan berhasil dibatalkan', + variant: 'default', + }) + await getPatientList() // Refresh list + } else { + const errorMessage = result.body?.message || 'Gagal membatalkan kunjungan' + toast({ + title: 'Gagal', + description: errorMessage, + variant: 'destructive', + }) + } + } catch (error: any) { + console.error('Error cancellation encounter:', error) + toast({ + title: 'Gagal', + description: error?.message || 'Gagal membatalkan kunjungan', + variant: 'destructive', + }) + } finally { + // Reset state + recId.value = 0 + recAction.value = '' + recItem.value = null + isRecordCancelOpen.value = false + } + } +} + // Handle confirmation result async function handleConfirmDelete(record: any, action: string) { if (action === 'delete' && record?.id) { @@ -152,6 +201,14 @@ async function handleConfirmDelete(record: any, action: string) { } function handleCancelConfirmation() { + // Reset record state when cancelled + recId.value = 0 + recAction.value = '' + recItem.value = null + isRecordCancelOpen.value = false +} + +function handleRemoveConfirmation() { // Reset record state when cancelled recId.value = 0 recAction.value = '' @@ -166,16 +223,21 @@ watch( isRecordConfirmationOpen.value = true return } - + + if (recAction.value === ActionEvents.showCancel) { + isRecordCancelOpen.value = true + return + } + 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 if (recAction.value === 'showPrint') { + console.log('print') } else { // handle other actions } @@ -194,10 +256,16 @@ watch( }, ) +watch(getActiveRole, () => { + const activeRole = getActiveRole() + activePosition.value = getPositionAs(activeRole) +}) + provide('rec_id', recId) provide('rec_action', recAction) provide('rec_item', recItem) provide('table_data_loader', isLoading) +provide('position', activePosition) onMounted(() => { getPatientList() @@ -234,12 +302,35 @@ onMounted(() => { + + + + + Nama: + {{ record.patient.person.name }} + + + No RM: + {{ record.medical_record_number }} + + + + + diff --git a/app/components/content/encounter/process-next.vue b/app/components/content/encounter/process-next.vue new file mode 100644 index 00000000..9fede8fb --- /dev/null +++ b/app/components/content/encounter/process-next.vue @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/components/content/sep/entry.vue b/app/components/content/sep/entry.vue index 3a916c4d..6e91f464 100644 --- a/app/components/content/sep/entry.vue +++ b/app/components/content/sep/entry.vue @@ -1,483 +1,59 @@ @@ -169,100 +85,77 @@ provide('table_data_loader', isLoading) - + + + + + + - + {{ dateRange }} - + - + - - + + Ekspor - Ekspor CSV - Ekspor Excel + Ekspor CSV + Ekspor Excel - + - + - Hapus SEP - Apakah anda yakin ingin menghapus SEP dengan data: - - No. SEP : {{ sepData.no_sep }} - No. Kartu BPJS : {{ sepData.kartu }} - Nama Pasien : {{ sepData.nama }} + No. SEP: {{ sepData.sepNumber }} + No. Kartu BPJS: {{ sepData.cardNumber }} + Nama Pasien: {{ sepData.patientName }} - - + { + recId = 0 + recAction = '' + open = false + }"> Tidak - + Ya diff --git a/app/components/pub/my-ui/comp-menu/comp-menu.vue b/app/components/pub/my-ui/comp-menu/comp-menu.vue new file mode 100644 index 00000000..fab41dde --- /dev/null +++ b/app/components/pub/my-ui/comp-menu/comp-menu.vue @@ -0,0 +1,45 @@ + + + + + + + + + {{ menu.label }} + + + + + + + + + + diff --git a/app/components/pub/my-ui/comp-tab/type.ts b/app/components/pub/my-ui/comp-tab/type.ts index ba21d0b7..6ca710fa 100644 --- a/app/components/pub/my-ui/comp-tab/type.ts +++ b/app/components/pub/my-ui/comp-tab/type.ts @@ -3,5 +3,7 @@ export interface TabItem { label: string component?: any groups?: string[] + classCode?: string[] + subClassCode?: string[] props?: Record } diff --git a/app/components/pub/my-ui/content-switcher/content-switcher.vue b/app/components/pub/my-ui/content-switcher/content-switcher.vue new file mode 100644 index 00000000..51d843ae --- /dev/null +++ b/app/components/pub/my-ui/content-switcher/content-switcher.vue @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/components/pub/my-ui/data/types.ts b/app/components/pub/my-ui/data/types.ts index 0d747580..cdbb81f9 100644 --- a/app/components/pub/my-ui/data/types.ts +++ b/app/components/pub/my-ui/data/types.ts @@ -71,6 +71,7 @@ export interface KeyNames { export interface LinkItem { label: string + value?: string icon?: string href?: string // to cover the needs of stating full external origins full url action?: string // for local paths @@ -84,6 +85,7 @@ export const ActionEvents = { showEdit: 'showEdit', showDetail: 'showDetail', showProcess: 'showProcess', + showCancel: 'showCancel', showVerify: 'showVerify', showValidate: 'showValidate', showPrint: 'showPrint', diff --git a/app/components/pub/my-ui/menus/submenu.vue b/app/components/pub/my-ui/menus/submenu.vue new file mode 100644 index 00000000..6875637f --- /dev/null +++ b/app/components/pub/my-ui/menus/submenu.vue @@ -0,0 +1,39 @@ + + + + + + + + + {{ menu.title }} + + + + + + + + + diff --git a/app/handlers/encounter-entry.handler.ts b/app/handlers/encounter-entry.handler.ts new file mode 100644 index 00000000..71594ebf --- /dev/null +++ b/app/handlers/encounter-entry.handler.ts @@ -0,0 +1,596 @@ +import { ref, reactive, computed } from 'vue' +import { useRoute } from 'vue-router' + +// Components +import { toast } from '~/components/pub/ui/toast' + +// Models +import type { TreeItem } from '~/components/pub/my-ui/select-tree/type' +import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type' +import { paymentTypes, sepRefTypeCodes, participantGroups } from '~/lib/constants.vclaim' + +// Stores +import { useUserStore } from '~/stores/user' + +// 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' + +// Handlers +import { + patients, + selectedPatient, + selectedPatientObject, + paginationMeta, + getPatientsList, + getPatientCurrent, + getPatientByIdentifierSearch, +} from '~/handlers/patient.handler' + +export function useEncounterEntry(props: { + id: number + classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient' + subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk' +}) { + const route = useRoute() + const userStore = useUserStore() + const openPatient = ref(false) + const isLoading = reactive({ + isTableLoading: false, + }) + const paymentsList = ref>([]) + const sepsList = ref>([]) + const participantGroupsList = ref>([]) + const specialistsTree = ref([]) + const specialistsData = ref([]) + const doctorsList = ref>([]) + const recSelectId = ref(null) + const isSaving = ref(false) + const isLoadingDetail = ref(false) + const encounterData = ref(null) + const formObjects = ref({}) + const isSepValid = ref(false) + const isCheckingSep = ref(false) + const sepNumber = ref('') + const vclaimReference = ref(null) + + const isEditMode = computed(() => props.id > 0) + + const isSaveDisabled = computed(() => { + return !selectedPatient.value || !selectedPatientObject.value || isSaving.value || isLoadingDetail.value + }) + + 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' + } + + 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 = getIsSubspecialist(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-vclaim/sep/add' + `?${queryParams.toString()}`) + } + + function getIsSubspecialist(value: string, items: TreeItem[]): boolean { + for (const item of items) { + if (item.value === value) { + return false + } + if (item.children) { + for (const child of item.children) { + if (child.value === value) { + return true + } + } + } + } + return false + } + + function getSpecialistCodeFromId(id: number | null | undefined): string | null { + if (!id) return null + + if (encounterData.value?.specialist?.id === id) { + return encounterData.value.specialist.code || null + } + + for (const specialist of specialistsData.value) { + if (specialist.id === id) { + return specialist.code || null + } + if (specialist.subspecialists && Array.isArray(specialist.subspecialists)) { + for (const subspecialist of specialist.subspecialists) { + if (subspecialist.id === id) { + return subspecialist.code || null + } + } + } + } + + return null + } + + function getSubspecialistCodeFromId(id: number | null | undefined): string | null { + if (!id) return null + + if (encounterData.value?.subspecialist?.id === id) { + return encounterData.value.subspecialist.code || null + } + + 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 + } + + function getSpecialistIdsFromCode(code: string): { specialist_id: number | null; subspecialist_id: number | null } { + if (!code) { + return { specialist_id: null, subspecialist_id: null } + } + + const isSub = getIsSubspecialist(code, specialistsTree.value) + + if (isSub) { + 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 { + 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 getValidateSepNumber(sepNumberValue: string) { + vclaimReference.value = null + if (!sepNumberValue || sepNumberValue.trim() === '') { + isSepValid.value = false + isCheckingSep.value = false + return + } + + try { + isSepValid.value = false + isCheckingSep.value = true + const result = await getSepList({ number: sepNumberValue.trim() }) + if (result.success && result.body?.response !== null) { + const response = result.body?.response || {} + if (Object.keys(response).length > 0) { + formObjects.value.patientName = response.peserta?.nama || '-' + formObjects.value.medicalRecordNumber = response.peserta?.noMr || '-' + formObjects.value.cardNumber = response.peserta?.noKartu || '-' + formObjects.value.registerDate = response.tglSep || null + vclaimReference.value = { + noSep: response.noSep || sepNumberValue.trim(), + tglRujukan: response.tglSep ? new Date(response.tglSep).toISOString() : null, + ppkDirujuk: response.noRujukan || 'rssa', + jnsPelayanan: response.jnsPelayanan === 'Rawat Jalan' ? '2' : response.jnsPelayanan === 'Rawat Inap' ? '1' : null, + catatan: response.catatan || '', + diagRujukan: response.diagnosa || '', + tipeRujukan: response.tujuanKunj?.kode ?? '0', + poliRujukan: response.poli || '', + user: '', + } + } + isSepValid.value = result.body?.metaData?.code === '200' + } + } catch (error) { + console.error('Error checking SEP:', error) + isSepValid.value = false + } finally { + isCheckingSep.value = false + } + } + + async function handleFetchSpecialists() { + try { + const specialistsResult = await getSpecialistList({ 'page-size': 100, includes: 'subspecialists' }) + if (specialistsResult.success) { + const specialists = specialistsResult.body?.data || [] + specialistsData.value = specialists + specialistsTree.value = getSpecialistTreeItems(specialists) + } + } catch (error) { + console.error('Error fetching specialist-subspecialist tree:', error) + } + } + + async function handleFetchDoctors(subSpecialistId: string | null = null) { + try { + const filterParams: any = { 'page-size': 100, includes: 'employee-Person' } + + if (!subSpecialistId) { + const doctors = await getDoctorValueLabelList(filterParams, true) + doctorsList.value = doctors + return + } + + const isSub = getIsSubspecialist(subSpecialistId, specialistsTree.value) + + if (isSub) { + filterParams['subspecialist-id'] = subSpecialistId + } else { + filterParams['specialist-id'] = subSpecialistId + } + + const doctors = await getDoctorValueLabelList(filterParams, true) + doctorsList.value = doctors + } catch (error) { + console.error('Error fetching doctors:', error) + doctorsList.value = [] + } + } + + 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 + await handleFetchDoctors() + await handleFetchSpecialists() + if (route.query) { + formObjects.value = { ...formObjects.value } + const queries = route.query as any + if (queries['sep-number']) { + formObjects.value.sepNumber = queries['sep-number'] + formObjects.value.paymentType = 'jkn' + } + } + } + + 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', + }) + if (result.success && result.body?.data) { + encounterData.value = result.body.data + await mapEncounterToForm(encounterData.value) + isLoadingDetail.value = false + } else { + toast({ + title: 'Gagal', + description: 'Gagal memuat data kunjungan', + variant: 'destructive', + }) + 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', + }) + await navigateTo(getListPath()) + } finally { + isLoadingDetail.value = false + } + } + + async function mapEncounterToForm(encounter: any) { + if (!encounter) return + + if (encounter.patient) { + selectedPatient.value = String(encounter.patient.id) + selectedPatientObject.value = encounter.patient + if (!encounter.patient.person) { + await getPatientCurrent(selectedPatient.value) + } + } + + const formData: any = {} + 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) { + formData.patientName = selectedPatientObject.value.person.name || '' + formData.nationalIdentity = selectedPatientObject.value.person.residentIdentityNumber || '' + formData.medicalRecordNumber = selectedPatientObject.value.number || '' + } + + const doctorId = encounter.appointment_doctor_id || encounter.responsible_doctor_id + if (doctorId) { + formData.doctorId = String(doctorId) + } + + 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 + } + } + + if (!formData.subSpecialistId) { + if (encounter.subspecialist?.code) { + formData.subSpecialistId = encounter.subspecialist.code + } else if (encounter.specialist?.code) { + formData.subSpecialistId = encounter.specialist.code + } + } + + if (encounter.registeredAt) { + 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] + } + + if (encounter.paymentMethod_code) { + if (encounter.paymentMethod_code === 'insurance') { + formData.paymentType = 'jkn' + } else { + const validPaymentTypes = ['jkn', 'jkmm', 'spm', 'pks'] + if (validPaymentTypes.includes(encounter.paymentMethod_code)) { + formData.paymentType = encounter.paymentMethod_code + } else { + formData.paymentType = 'spm' + } + } + } else { + formData.paymentType = 'spm' + } + + formData.cardNumber = encounter.member_number || '' + formData.sepNumber = encounter.ref_number || '' + formObjects.value = formData + + if (formData.sepNumber) { + sepNumber.value = formData.sepNumber + } + if (formData.subSpecialistId) { + await handleFetchDoctors(formData.subSpecialistId) + } + } + + async function handleSaveEncounter(formValues: any) { + if (!selectedPatient.value || !selectedPatientObject.value) { + toast({ + title: 'Gagal', + description: 'Pasien harus dipilih terlebih dahulu', + variant: 'destructive', + }) + return + } + + try { + isSaving.value = true + + const employeeId = userStore.user?.employee_id || userStore.user?.employee?.id || 0 + + const formatDate = (dateString: string): string => { + if (!dateString) return '' + const date = new Date(dateString) + return date.toISOString() + } + + const { specialist_id, subspecialist_id } = getSpecialistIdsFromCode(formValues.subSpecialistId || '') + + const patientId = formValues.patient_id || selectedPatientObject.value?.id || Number(selectedPatient.value) + + const registeredAtValue = formValues.registeredAt || formValues.registerDate || '' + const visitDateValue = formValues.visitDate || formValues.registeredAt || formValues.registerDate || '' + const memberNumber = formValues.member_number ?? formValues.cardNumber ?? formValues.memberNumber ?? null + const refNumber = formValues.ref_number ?? formValues.sepNumber ?? formValues.refNumber ?? null + + let paymentMethodCode = formValues.paymentMethod_code ?? null + if (!paymentMethodCode) { + if (formValues.paymentType === 'jkn' || formValues.paymentType === 'jkmm') { + paymentMethodCode = 'insurance' + } else if (formValues.paymentType === 'spm') { + paymentMethodCode = 'cash' + } else if (formValues.paymentType === 'pks') { + paymentMethodCode = 'membership' + } else { + paymentMethodCode = 'cash' + } + } + + const payload: any = { + patient_id: patientId, + appointment_doctor_code: formValues.doctorId || null, + class_code: props.classCode || '', + subClass_code: props.subClassCode || '', + infra_id: formValues.infra_id ?? null, + unit_code: userStore?.user?.unit_code ?? null, + refSource_name: formValues.refSource_name ?? 'RSSA', + refTypeCode: formValues.paymentType === 'jkn' ? 'bpjs' : '', + vclaimReference: vclaimReference.value ?? null, + paymentType: formValues.paymentType, + registeredAt: formatDate(registeredAtValue), + visitDate: formatDate(visitDateValue), + } + + if (props.classCode !== 'inpatient') { + delete payload.infra_id + } + if (employeeId && employeeId > 0) { + payload.adm_employee_id = employeeId + } + if (specialist_id) { + payload.specialist_id = specialist_id + } + if (subspecialist_id) { + payload.subspecialist_id = subspecialist_id + } + if (paymentMethodCode) { + payload.paymentMethod_code = paymentMethodCode + } + + if (paymentMethodCode === 'insurance') { + payload.insuranceCompany_id = formValues.insuranceCompany_id ?? null + if (memberNumber) payload.member_number = memberNumber + if (refNumber) payload.ref_number = refNumber + if (formValues.refTypeCode) payload.refTypeCode = formValues.refTypeCode + if (formValues.vclaimReference) payload.vclaimReference = formValues.vclaimReference + } else { + if (paymentMethodCode === 'membership' && memberNumber) { + payload.member_number = memberNumber + } + if (refNumber) { + payload.ref_number = refNumber + } + } + + if (props.classCode === 'ambulatory') { + payload.visitMode_code = 'adm' + payload.allocatedVisitCount = 0 + } + + 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', + }) + 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 + } + } + + return { + patients, + paymentsList, + sepsList, + sepNumber, + participantGroupsList, + specialistsTree, + doctorsList, + recSelectId, + isSaving, + isLoadingDetail, + encounterData, + formObjects, + openPatient, + isSepValid, + isCheckingSep, + isEditMode, + isSaveDisabled, + isLoading, + selectedPatient, + selectedPatientObject, + paginationMeta, + loadEncounterDetail, + mapEncounterToForm, + toKebabCase, + toNavigateSep, + getListPath, + getSpecialistCodeFromId, + getSubspecialistCodeFromId, + getIsSubspecialist, + getSpecialistIdsFromCode, + getPatientsList, + getPatientCurrent, + getPatientByIdentifierSearch, + getValidateSepNumber, + handleFetchSpecialists, + handleFetchDoctors, + handleInit, + handleSaveEncounter, + } +} diff --git a/app/handlers/encounter-init.handler.ts b/app/handlers/encounter-init.handler.ts new file mode 100644 index 00000000..95e4e27d --- /dev/null +++ b/app/handlers/encounter-init.handler.ts @@ -0,0 +1,489 @@ +import { isValidDate } from '~/lib/date' +import { medicalPositions } from '~/lib/roles' + +export interface EncounterItem { + id: string + title: string + classCode?: string[] + unit?: string + afterId?: string + component?: any + props?: Record +} + +export interface EncounterProps { + classCode: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient' + subClassCode: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk' +} + +export interface EncounterListData { + encounter?: any + status?: any + medicalAssessment?: any + medicalAssessmentRehab: any + functionAssessment?: any + protocolTheraphy?: any + protocolChemotherapy?: any + medicineProtocolChemotherapy?: any + consultation?: any + letterOfControl?: any +} + +const StatusAsync = defineAsyncComponent(() => import('~/components/content/encounter/status.vue')) +const AssesmentFunctionListAsync = defineAsyncComponent(() => import('~/components/content/soapi/entry.vue')) +const EarlyMedicalAssesmentListAsync = defineAsyncComponent(() => import('~/components/content/soapi/entry.vue')) +const EarlyMedicalRehabListAsync = defineAsyncComponent(() => import('~/components/content/soapi/entry.vue')) +const ChemoProtocolListAsync = defineAsyncComponent(() => import('~/components/app/chemotherapy/list.protocol.vue')) +const ChemoMedicineProtocolListAsync = defineAsyncComponent( + () => import('~/components/app/chemotherapy/list.medicine.vue'), +) +const DeviceOrderAsync = defineAsyncComponent(() => import('~/components/content/device-order/main.vue')) +const PrescriptionAsync = defineAsyncComponent(() => import('~/components/content/prescription/main.vue')) +const CpLabOrderAsync = defineAsyncComponent(() => import('~/components/content/cp-lab-order/main.vue')) +const CprjAsync = defineAsyncComponent(() => import('~/components/content/cprj/entry.vue')) +const RadiologyAsync = defineAsyncComponent(() => import('~/components/content/radiology-order/main.vue')) +const ConsultationAsync = defineAsyncComponent(() => import('~/components/content/consultation/list.vue')) +const DocUploadListAsync = defineAsyncComponent(() => import('~/components/content/document-upload/list.vue')) +const GeneralConsentListAsync = defineAsyncComponent(() => import('~/components/content/general-consent/entry.vue')) +const ResumeListAsync = defineAsyncComponent(() => import('~/components/content/resume/list.vue')) +const ControlLetterListAsync = defineAsyncComponent(() => import('~/components/content/control-letter/list.vue')) + +const defaultKeys: Record = { + status: { + id: 'status', + title: 'Status Masuk/Keluar', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + earlyMedicalAssessment: { + id: 'early-medical-assessment', + title: 'Pengkajian Awal Medis', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + rehabMedicalAssessment: { + id: 'rehab-medical-assessment', + title: 'Pengkajian Awal Medis Rehabilitasi Medis', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'rehab', + afterId: 'early-medical-assessment', + }, + functionAssessment: { + id: 'function-assessment', + title: 'Asesmen Fungsi', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'rehab', + afterId: 'rehab-medical-assessment', + }, + therapyProtocol: { + id: 'therapy-protocol', + classCode: ['ambulatory'], + title: 'Protokol Terapi', + unit: 'rehab', + afterId: 'function-assessment', + }, + chemotherapyProtocol: { + id: 'chemotherapy-protocol', + title: 'Protokol Kemoterapi', + classCode: ['ambulatory'], + unit: 'chemo', + afterId: 'early-medical-assessment', + }, + chemotherapyMedicine: { + id: 'chemotherapy-medicine', + title: 'Protokol Obat Kemoterapi', + classCode: ['ambulatory'], + unit: 'chemo', + afterId: 'chemotherapy-protocol', + }, + educationAssessment: { + id: 'education-assessment', + title: 'Asesmen Kebutuhan Edukasi', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + consent: { + id: 'consent', + title: 'General Consent', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + patientNote: { + id: 'patient-note', + title: 'CPRJ', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + prescription: { + id: 'prescription', + title: 'Order Obat', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + device: { + id: 'device-order', + title: 'Order Alkes', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + mcuRadiology: { + id: 'mcu-radiology', + title: 'Order Radiologi', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + mcuLabPc: { + id: 'mcu-lab-pc', + title: 'Order Lab PK', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + mcuLabMicro: { + id: 'mcu-lab-micro', + title: 'Order Lab Mikro', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + mcuLabPa: { + id: 'mcu-lab-pa', + title: 'Order Lab PA', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + medicalAction: { + id: 'medical-action', + title: 'Order Ruang Tindakan', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + mcuResult: { + id: 'mcu-result', + title: 'Hasil Penunjang', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + consultation: { + id: 'consultation', + title: 'Konsultasi', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + resume: { + id: 'resume', + title: 'Resume', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + control: { + id: 'control', + title: 'Surat Kontrol', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + screening: { + id: 'screening', + title: 'Skrinning MPP', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, + supportingDocument: { + id: 'supporting-document', + title: 'Upload Dokumen Pendukung', + classCode: ['ambulatory'], + unit: 'rehab', + }, + priceList: { + id: 'price-list', + title: 'Tarif Tindakan', + classCode: ['ambulatory', 'emergency', 'inpatient'], + unit: 'all', + }, +} + +export function getItemsByClassCode(classCode: string, items: EncounterItem[]) { + return items.filter((item) => item.classCode?.includes(classCode)) +} + +export function getItemsByUnit(unit: string, items: EncounterItem[]) { + return items.filter((item) => item.unit === unit) +} + +export function getItemsByIds(ids: string[], items: EncounterItem[]) { + return items.filter((item) => ids.includes(item.id)) +} + +export function getIndexById(id: string, items: EncounterItem[]) { + return items.findIndex((item) => item.id === id) +} + +export const getItemsAll = (classCode: string, unit: string, items: EncounterItem[]) => { + const prevItems = [...items] + let updateItems = getItemsByClassCode(classCode, prevItems) + updateItems = getItemsByUnit(unit, updateItems) + return updateItems +} + +export function insertItemByAfterId(id: string, items: EncounterItem[], newItem: EncounterItem) { + const index = getIndexById(id, items) + if (index > -1) { + items.splice(index + 1, 0, newItem) + } +} + +export function injectComponents(id: string | number, data: EncounterListData, meta: EncounterListData) { + const currentKeys = { ...defaultKeys } + if (currentKeys?.status) { + currentKeys.status['component'] = StatusAsync + currentKeys.status['props'] = { encounter: data?.encounter } + } + if (currentKeys?.earlyMedicalAssessment) { + currentKeys.earlyMedicalAssessment['component'] = EarlyMedicalAssesmentListAsync + currentKeys.earlyMedicalAssessment['props'] = { + encounter: data?.encounter, + type: 'early-medic', + label: currentKeys.earlyMedicalAssessment['title'], + } + } + if (currentKeys?.rehabMedicalAssessment) { + currentKeys.rehabMedicalAssessment['component'] = EarlyMedicalRehabListAsync + currentKeys.rehabMedicalAssessment['props'] = { + encounter: data?.encounter, + type: 'early-rehab', + label: currentKeys.rehabMedicalAssessment['title'], + } + } + if (currentKeys?.functionAssessment) { + currentKeys.functionAssessment['component'] = AssesmentFunctionListAsync + currentKeys.functionAssessment['props'] = { + encounter: data?.encounter, + type: 'function', + label: currentKeys.functionAssessment['title'], + } + } + if (currentKeys?.therapyProtocol) { + // TODO: add component for therapyProtocol + currentKeys.therapyProtocol['component'] = null + currentKeys.therapyProtocol['props'] = { + data: data?.encounter, + paginationMeta: meta?.protocolTheraphy, + } + } + if (currentKeys?.chemotherapyProtocol) { + currentKeys.chemotherapyProtocol['component'] = ChemoProtocolListAsync + currentKeys.chemotherapyProtocol['props'] = { + data: data?.encounter, + paginationMeta: meta?.protocolChemotherapy, + } + } + if (currentKeys?.chemotherapyMedicine) { + currentKeys.chemotherapyMedicine['component'] = ChemoMedicineProtocolListAsync + currentKeys.chemotherapyMedicine['props'] = { + data: data?.encounter, + paginationMeta: meta?.medicineProtocolChemotherapy, + } + } + if (currentKeys?.educationAssessment) { + // TODO: add component for education assessment + currentKeys.educationAssessment['component'] = null + currentKeys.educationAssessment['props'] = { encounter_id: id } + } + if (currentKeys?.consent) { + currentKeys.consent['component'] = GeneralConsentListAsync + currentKeys.consent['props'] = { encounter_id: id } + } + if (currentKeys?.patientNote) { + currentKeys.patientNote['component'] = CprjAsync + currentKeys.patientNote['props'] = { encounter_id: id } + } + if (currentKeys?.prescription) { + currentKeys.prescription['component'] = PrescriptionAsync + currentKeys.prescription['props'] = { encounter_id: id } + } + if (currentKeys?.device) { + currentKeys.device['component'] = DeviceOrderAsync + currentKeys.device['props'] = { encounter_id: id } + } + if (currentKeys?.mcuRadiology) { + currentKeys.mcuRadiology['component'] = RadiologyAsync + currentKeys.mcuRadiology['props'] = { encounter_id: id } + } + if (currentKeys?.mcuLabPc) { + currentKeys.mcuLabPc['component'] = CpLabOrderAsync + currentKeys.mcuLabPc['props'] = { encounter_id: id } + } + if (currentKeys?.mcuLabMicro) { + // TODO: add component for mcuLabMicro + currentKeys.mcuLabMicro['component'] = null + currentKeys.mcuLabMicro['props'] = { encounter_id: id } + } + if (currentKeys?.mcuLabPa) { + // TODO: add component for mcuLabPa + currentKeys.mcuLabPa['component'] = null + currentKeys.mcuLabPa['props'] = { encounter_id: id } + } + if (currentKeys?.medicalAction) { + // TODO: add component for medicalAction + currentKeys.medicalAction['component'] = null + currentKeys.medicalAction['props'] = { encounter_id: id } + } + if (currentKeys?.mcuResult) { + // TODO: add component for mcuResult + currentKeys.mcuResult['component'] = null + currentKeys.mcuResult['props'] = { encounter_id: id } + } + if (currentKeys?.consultation) { + currentKeys.consultation['component'] = ConsultationAsync + currentKeys.consultation['props'] = { encounter: data?.encounter } + } + if (currentKeys?.resume) { + currentKeys.resume['component'] = ResumeListAsync + currentKeys.resume['props'] = { encounter_id: id } + } + if (currentKeys?.control) { + currentKeys.control['component'] = ControlLetterListAsync + currentKeys.control['props'] = { encounter: data?.encounter } + } + if (currentKeys?.screening) { + // TODO: add component for screening + currentKeys.screening['component'] = null + currentKeys.screening['props'] = { encounter_id: id } + } + if (currentKeys?.supportingDocument) { + currentKeys.supportingDocument['component'] = DocUploadListAsync + currentKeys.supportingDocument['props'] = { encounter_id: id } + } + if (currentKeys?.priceList) { + // TODO: add component for priceList + currentKeys.priceList['component'] = null + currentKeys.priceList['props'] = { encounter_id: id } + } + + return currentKeys +} + +export function mergeArrayAt(arraysOne: T[], arraysTwo: T[] | T, deleteCount = 0): T[] { + const prevItems = arraysOne.slice() + if (!prevItems) return prevItems + const nextItems = Array.isArray(arraysTwo) ? arraysTwo : [arraysTwo] + if (nextItems.length === 0) return prevItems + // determine insertion position using the first item's `id` if available + const firstId = (nextItems[0] as any)?.afterId || (prevItems[0] as any)?.id + let pos = prevItems.length + if (typeof firstId === 'string') { + const index = prevItems.findIndex((item: any) => item.id === firstId) + pos = index < 0 ? Math.max(prevItems.length + index, 0) : Math.min(index, prevItems.length) + } + prevItems.splice(pos, deleteCount, ...nextItems) + return prevItems +} + +// Function to map API response to Encounter structure +export function mapResponseToEncounter(result: any): any { + if (!result) return null + + // Check if patient and patient.person exist (minimal validation) + if (!result.patient || !result.patient.person) { + return null + } + + const mapped: any = { + id: result.id || 0, + patient_id: result.patient_id || result.patient?.id || 0, + patient: { + id: result.patient?.id || 0, + number: result.patient?.number || '', + person: { + id: result.patient?.person?.id || 0, + name: result.patient?.person?.name || '', + birthDate: result.patient?.person?.birthDate || null, + gender_code: result.patient?.person?.gender_code || '', + residentIdentityNumber: result.patient?.person?.residentIdentityNumber || null, + frontTitle: result.patient?.person?.frontTitle || '', + endTitle: result.patient?.person?.endTitle || '', + addresses: result.patient?.person?.addresses || [], + }, + }, + registeredAt: result.registeredAt || result.patient?.registeredAt || null, + class_code: result.class_code || '', + unit_id: result.unit_id || 0, + unit: result.unit || null, + specialist_id: result.specialist_id || null, + subspecialist_id: result.subspecialist_id || null, + visitDate: isValidDate(result.visitDate) + ? result.visitDate + : result.registeredAt || result.patient?.registeredAt || null, + adm_employee_id: result.adm_employee_id || 0, + appointment_doctor_id: result.appointment_doctor_id || null, + responsible_doctor_id: result.responsible_doctor_id || null, + appointment_doctor: result.appointment_doctor || null, + responsible_doctor: result.responsible_doctor || null, + refSource_name: result.refSource_name || null, + appointment_id: result.appointment_id || null, + earlyEducation: result.earlyEducation || null, + medicalDischargeEducation: result.medicalDischargeEducation || '', + admDischargeEducation: result.admDischargeEducation || null, + discharge_method_code: result.discharge_method_code || null, + discharge_reason: result.dischargeReason || result.discharge_reason || null, + discharge_date: result.discharge_date || null, + status_code: result.status_code || '', + // Payment related fields + paymentMethod_code: + result.paymentMethod_code && result.paymentMethod_code.trim() !== '' ? result.paymentMethod_code : null, + trx_number: result.trx_number || null, + member_number: result.member_number || null, + ref_number: result.ref_number || null, + } + + return mapped +} + +export function getMenuItems( + id: string | number, + props: any, + user: any, + data: EncounterListData, + meta: any, +) { + const normalClassCode = props.classCode === 'ambulatory' ? 'outpatient' : props.classCode + const currentKeys = injectComponents(id, data, meta) + const defaultItems: EncounterItem[] = Object.values(currentKeys) + const listItemsForOutpatientRehab = mergeArrayAt( + getItemsAll('ambulatory', 'all', defaultItems), + getItemsAll('ambulatory', 'rehab', defaultItems), + ) + const listItemsForOutpatientChemo = mergeArrayAt( + getItemsAll('ambulatory', 'all', defaultItems), + getItemsAll('ambulatory', 'chemo', defaultItems), + ) + const listItems: Record>> = { + 'installation|outpatient': { + 'unit|rehab': { + items: listItemsForOutpatientRehab, + roles: medicalPositions, + }, + 'unit|chemo': { + items: listItemsForOutpatientChemo, + roles: medicalPositions, + }, + all: getItemsAll('ambulatory', 'all', defaultItems), + }, + 'installation|emergency': { + all: getItemsAll('emergency', 'all', defaultItems), + }, + 'installation|inpatient': { + all: getItemsAll('inpatient', 'all', defaultItems), + }, + } + const currentListItems = listItems[`installation|${normalClassCode}`] + if (!currentListItems) return [] + const unitCode = user?.unit_code ? `unit|${user.unit_code}` : 'all' + const currentUnitItems: any = currentListItems[`${unitCode}`] + if (!currentUnitItems) return [] + let menus = [] + if (currentUnitItems.roles && currentUnitItems.roles?.includes(user.activeRole)) { + menus = [...currentUnitItems.items] + } else { + menus = unitCode !== 'all' && currentUnitItems?.items ? [...currentUnitItems.items] : [...currentUnitItems] + } + return menus +} diff --git a/app/handlers/encounter-process.handler.ts b/app/handlers/encounter-process.handler.ts new file mode 100644 index 00000000..67850da4 --- /dev/null +++ b/app/handlers/encounter-process.handler.ts @@ -0,0 +1,32 @@ +// Services +import { getDetail } from '~/services/encounter.service' + +// Handlers +import { mapResponseToEncounter } from '~/handlers/encounter-init.handler' + +export async function getEncounterData(id: string | number) { + let data = null + try { + const dataRes = await getDetail(id, { + includes: + 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person,Responsible_Doctor,Responsible_Doctor-employee,Responsible_Doctor-employee-person', + }) + const dataResBody = dataRes.body ?? null + const result = dataResBody?.data ?? null + + if (result) { + const mappedData = mapResponseToEncounter(result) + if (mappedData) { + data = mappedData + } else { + data = null + } + } else { + data = null + } + } catch (error) { + console.error('Error fetching encounter data:', error) + data = null + } + return data +} \ No newline at end of file diff --git a/app/handlers/integration-sep-entry.handler.ts b/app/handlers/integration-sep-entry.handler.ts new file mode 100644 index 00000000..ad267bef --- /dev/null +++ b/app/handlers/integration-sep-entry.handler.ts @@ -0,0 +1,691 @@ +import { ref } from 'vue' +import { useRoute } from 'vue-router' + +// Components +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' + +export function useIntegrationSepEntry() { + const userStore = useUserStore() + const route = useRoute() + + const openPatient = ref(false) + const openLetter = ref(false) + const openHistory = ref(false) + const selectedLetter = ref('') + const selectedObjects = ref({}) + const selectedServiceType = ref('') + const selectedAdmissionType = ref('') + const histories = ref>([]) + const letters = ref>([]) + const doctors = ref>([]) + const diagnoses = ref>([]) + const facilitiesFrom = ref>([]) + const facilitiesTo = ref>([]) + const supportCodesList = ref>([]) + const serviceTypesList = ref>([]) + const registerMethodsList = ref>([]) + const accidentsList = ref>([]) + const purposeOfVisitsList = ref>([]) + const proceduresList = ref>([]) + const assessmentsList = ref>([]) + const provincesList = ref>([]) + const citiesList = ref>([]) + const districtsList = ref>([]) + const classLevelsList = ref>([]) + const classLevelUpgradesList = ref>([]) + const classPaySourcesList = ref>([]) + const isServiceHidden = ref(false) + const isSaveLoading = ref(false) + const isLetterReadonly = ref(false) + const isLoadingPatient = ref(false) + const specialistsTree = ref([]) + const resourceType = ref('') + const resourcePath = ref('') + + /** + * Map letter data to form fields for save-sep + * Maps data from letters.value[0].information to selectedObjects and form values + */ + function mapLetterDataToForm(formValues: any): any { + if (selectedAdmissionType.value === '3' || letters.value.length === 0) { + return formValues + } + + const letterData = letters.value[0] + const info = letterData.information || {} + + // Map data to selectedObjects for form population + if (info.cardNumber) { + selectedObjects.value['cardNumber'] = info.cardNumber + } + if (info.medicalRecordNumber) { + selectedObjects.value['medicalRecordNumber'] = info.medicalRecordNumber + } + if (info.patientPhone) { + selectedObjects.value['phoneNumber'] = info.patientPhone + } + if (info.classLevel) { + selectedObjects.value['classLevel'] = info.classLevel + } + + // Map data to formValues for makeSepData + const mappedValues = { ...formValues } + + // response.rujukan.peserta.noKartu → cardNumber (noKartu) + if (info.cardNumber) { + mappedValues.cardNumber = info.cardNumber + } + + // response.rujukan.tglKunjungan → referralLetterDate (rujukan.tglRujukan) + if (letterData.plannedDate) { + mappedValues.referralLetterDate = letterData.plannedDate + } + + // response.rujukan.noKunjungan → referralLetterNumber (rujukan.noRujukan) + if (letterData.letterNumber) { + mappedValues.referralLetterNumber = letterData.letterNumber + } + + // response.rujukan.provPerujuk.kode → fromClinic (rujukan.ppkRujukan) + if (info.destination) { + mappedValues.referralTo = info.destination + } + + // response.rujukan.poliRujukan.kode → polyCode + if (info.poly) { + mappedValues.polyCode = info.poly + } + + // response.asalFaskes → asalRujukan (1 = Faskes 1, 2 = Faskes RS) + // Map facility to referralFrom (asalRujukan) + if (info.facility) { + mappedValues.referralFrom = info.facility + } + + // response.rujukan.diagnosa.kode → initialDiagnosis (diagAwal) + if (info.diagnoses) { + mappedValues.initialDiagnosis = info.diagnoses + } + + // response.rujukan.poliRujukan.kode → destinationClinic (poli.tujuan) + if (info.poly) { + mappedValues.destinationClinic = info.poly + } + + // response.rujukan.peserta.hakKelas.kode → classLevel (klsRawat.klsRawatHak) + if (info.classLevel) { + mappedValues.classLevel = info.classLevel + } + + // response.rujukan.peserta.mr.noMR → medicalRecordNumber (noMR) + if (info.medicalRecordNumber) { + mappedValues.medicalRecordNumber = info.medicalRecordNumber + } + + // response.rujukan.peserta.mr.noTelepon → phoneNumber (noTelp) + if (info.patientPhone) { + mappedValues.phoneNumber = info.patientPhone + } + + return mappedValues + } + + 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, + }) + }) + } + } + + 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 { + 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 || '', + diagnose: 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 || '', + identity: lettersRaw?.rujukan?.peserta?.nik || '', + patientName: lettersRaw?.rujukan?.peserta?.nama || '', + patientPhone: lettersRaw?.rujukan?.peserta?.mr?.noTelepon || '', + medicalRecordNumber: lettersRaw?.rujukan?.peserta?.mr?.noMR || '', + destination: lettersRaw?.rujukan?.provPerujuk?.kode || '', + }, + }, + ] + } + } + } + + 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 { + isLoadingPatient.value = true + 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 || '' + } + isLoadingPatient.value = false + } catch (err) { + console.error('Failed to load patient from query params:', err) + isLoadingPatient.value = false + } + } + + function handleSaveLetter() { + // 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 + } + } + + 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 (menu === 'service-type') { + selectedServiceType.value = value + doctors.value = await getDoctorLabelList({ + serviceType: selectedServiceType.value || '2', + 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') { + if (isLoadingPatient.value) return + const text = value.text + const type = value.type + const prevCardNumber = selectedObjects.value['cardNumber'] || '' + const prevNationalIdentity = selectedObjects.value['nationalIdentity'] || '' + if (type === 'indentity' && text !== prevNationalIdentity) { + 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(async () => { + if (letters.value.length > 0) { + const copyObjects = { ...selectedObjects.value } + const letter = letters.value[0] + selectedObjects.value = {} + selectedLetter.value = letter.letterNumber + isLetterReadonly.value = true + if (letter.information || letter.clinic) { + const poly = value.admissionType === '3' ? letter.clinic : letter.information?.poly + if (poly) { + const resultControl = await getControlLetterList({ + mode: 'by-schedule', + controlDate: letter.plannedDate, + controlType: selectedServiceType.value, + polyCode: poly, + }) + if (resultControl && resultControl.success && resultControl.body) { + const resultData = resultControl.body?.response?.list || [] + const resultUnique = [...new Map(resultData.map((item: any) => [item.kodeDokter, item])).values()] + const controlLetters = resultUnique.map((item: any) => ({ + value: item.kodeDokter ? String(item.kodeDokter) : '', + label: `${item.kodeDokter} - ${item.namaDokter} - ${item.jadwalPraktek} (${item.kapasitas})`, + })) + doctors.value = controlLetters + } + } + } + setTimeout(async () => { + selectedObjects.value = copyObjects + selectedObjects.value['letterDate'] = letter.plannedDate + selectedObjects.value['cardNumber'] = letter.information?.cardNumber || '' + selectedObjects.value['nationalIdentity'] = letter.information?.identity || '' + selectedObjects.value['medicalRecordNumber'] = letter.information?.medicalRecordNumber || '' + selectedObjects.value['patientName'] = letter.information?.patientName || '' + selectedObjects.value['phoneNumber'] = letter.information?.patientPhone || '' + selectedObjects.value['facility'] = letter.information?.facility || '' + selectedObjects.value['diagnose'] = letter.information?.diagnose || '' + selectedObjects.value['serviceType'] = letter.information?.serviceType || '' + selectedObjects.value['classLevel'] = letter.information?.classLevel || '' + selectedObjects.value['poly'] = letter.information?.poly || '' + selectedObjects.value['destination'] = letter.information?.destination || '' + if (!!selectedObjects.value['diagnose']) { + const diagnoseRes: any = await getDiagnoseLabelList({ diagnosa: selectedObjects.value['diagnose'] }) + diagnoses.value = diagnoseRes + if (diagnoseRes && diagnoseRes.length > 0) { + selectedObjects.value['diagnoseLabel'] = diagnoseRes[0].value + } + } + }, 250) + } + }) + return + } + if (menu === 'open-letter') { + openLetter.value = true + return + } + if (menu === 'history-sep') { + getMonitoringHistoryMappers().then(() => { + openHistory.value = true + }) + return + } + if (menu === 'sep-number-changed') { + // Update sepNumber when it changes in form (only if different to prevent loop) + } + if (menu === 'back') { + navigateTo('/integration/bpjs-vclaim/sep') + } + if (menu === 'save-sep') { + isSaveLoading.value = true + + // Map letter data to form if admissionType !== '3' and letters.value has data + let mappedValues = value + if (selectedAdmissionType.value !== '3') { + if (letters.value.length > 0) { + // Map data from letters.value to form values + mappedValues = mapLetterDataToForm(value) + } else { + // Fallback: use getPatientExternalMappers if letters.value is empty + // Get card number from form values or selectedObjects + const cardNumberToSearch = value.cardNumber || selectedObjects.value['cardNumber'] || '' + if (cardNumberToSearch && cardNumberToSearch !== '-') { + await getPatientExternalMappers(cardNumberToSearch, 'by-card') + // Update mappedValues with data from getPatientExternalMappers + if (selectedObjects.value['cardNumber']) { + mappedValues.cardNumber = selectedObjects.value['cardNumber'] + } + if (selectedObjects.value['medicalRecordNumber']) { + mappedValues.medicalRecordNumber = selectedObjects.value['medicalRecordNumber'] + } + if (selectedObjects.value['phoneNumber']) { + mappedValues.phoneNumber = selectedObjects.value['phoneNumber'] + } + if (selectedObjects.value['classLevel']) { + mappedValues.classLevel = selectedObjects.value['classLevel'] + } + } + } + } + + if (!value.destinationClinic) { + mappedValues.destinationClinic = selectedObjects.value['destination'] || '' + } + if (!value.clinicExcecutive) { + mappedValues.clinicExcecutive = 'no' + } + + mappedValues.userName = userStore.user?.user_name || '' + + createSep(makeSepData(mappedValues)) + .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 (!!resourcePath.value) { + navigateTo({ path: resourcePath.value, query: { 'sep-number': body?.response?.sep?.noSep || '-' } }) + return + } + navigateTo('/integration/bpjs-vclaim/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 || 2, + }) + } + if (menu === 'clinic-to') { + facilitiesTo.value = await getHealthFacilityLabelList({ + healthcare: value, + healthcareType: selectedServiceType.value || 2, + }) + } + 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() { + selectedServiceType.value = '2' + 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 || '2', + 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['source-path']) resourcePath.value = queries['source-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'] + } + } + + return { + openPatient, + openLetter, + openHistory, + selectedLetter, + selectedObjects, + selectedServiceType, + selectedAdmissionType, + histories, + letters, + doctors, + diagnoses, + facilitiesFrom, + facilitiesTo, + supportCodesList, + serviceTypesList, + registerMethodsList, + accidentsList, + purposeOfVisitsList, + proceduresList, + assessmentsList, + provincesList, + citiesList, + districtsList, + classLevelsList, + classLevelUpgradesList, + classPaySourcesList, + isServiceHidden, + isSaveLoading, + isLetterReadonly, + isLoadingPatient, + specialistsTree, + resourceType, + resourcePath, + patients, + selectedPatient, + paginationMeta, + getMonitoringHistoryMappers, + getLetterMappers, + getPatientInternalMappers, + getPatientExternalMappers, + getPatientsList, + getPatientByIdentifierSearch, + handleSaveLetter, + mapLetterDataToForm, + handleSavePatient, + handleEvent, + handleFetch, + handleFetchSpecialists, + handleInit, + } +} + +export default useIntegrationSepEntry diff --git a/app/handlers/integration-sep-list.handler.ts b/app/handlers/integration-sep-list.handler.ts new file mode 100644 index 00000000..94cdccce --- /dev/null +++ b/app/handlers/integration-sep-list.handler.ts @@ -0,0 +1,284 @@ +import { ref, reactive } from 'vue' +// Components +import { toast } from '~/components/pub/ui/toast' +// Types +import type { Ref as VueRef } from 'vue' +import type { DateRange } from 'radix-vue' +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' +import type { VclaimSepData } from '~/models/vclaim' +// Libraries +import { CalendarDate, getLocalTimeZone } from '@internationalized/date' +import { getFormatDateId } from '~/lib/date' +import { downloadCsv, downloadXls } from '~/lib/download' +import { serviceTypes } from '~/lib/constants.vclaim' +import { getList as geMonitoringVisitList } from '~/services/vclaim-monitoring-visit.service' +import { remove as removeSepData, makeSepDataForRemove } from '~/services/vclaim-sep.service' + +const headerKeys = [ + 'letterDate', + 'letterNumber', + 'serviceType', + 'flow', + 'medicalRecordNumber', + 'patientName', + 'cardNumber', + 'controlLetterNumber', + 'controlLetterDate', + 'clinicDestination', + 'attendingDoctor', + 'diagnosis', + 'careClass', +] + +const headerLabels = [ + 'Tanggal SEP', + 'No. SEP', + 'Jenis Pelayanan', + 'Alur', + 'No. Rekam Medis', + 'Nama Pasien', + 'No. Kartu BPJS', + 'No. Surat Kontrol', + 'Tgl Surat Kontrol', + 'Poli Tujuan', + 'Dokter Penanggung Jawab', + 'Diagnosa', + 'Kelas Perawatan', +] + +export function useIntegrationSepList() { + const userStore = useUserStore() + const today = new Date() + const initCalDate = (d: Date) => new CalendarDate(d.getFullYear(), d.getMonth() + 1, d.getDate()) + + const recId = ref(0) + const recAction = ref('') + const recItem = ref(null) + + const data = ref([]) + const dateSelection = ref({ start: initCalDate(today), end: initCalDate(today) }) as VueRef + const dateRange = ref(`${getFormatDateId(today)} - ${getFormatDateId(today)}`) + const serviceType = ref('2') + const serviceTypesList = ref([]) + const search = ref('') + const open = ref(false) + + const sepData = ref({ + sepNumber: '', + cardNumber: '', + patientName: '', + }) + + const refSearchNav: RefSearchNav = { + onClick: () => {}, + onInput: (_val: string) => {}, + onClear: () => {}, + } + + const headerPrep: HeaderPrep = { + title: 'Daftar SEP Prosedur', + icon: 'i-lucide-panel-bottom', + addNav: { + label: 'Tambah', + onClick: () => { + navigateTo('/integration/bpjs-vclaim/sep/add') + }, + }, + } + + const paginationMeta = reactive({ + recordCount: 0, + page: 1, + pageSize: 10, + totalPage: 5, + hasNext: false, + hasPrev: false, + }) + + const isLoading = reactive({ + isTableLoading: false, + }) + + const getDateFilter = () => { + let dateFilter = '' + const isTimeLocal = true + const dateFirst = + dateSelection.value && dateSelection.value.start + ? dateSelection.value.start.toDate(getLocalTimeZone()) + : new Date() + if (isTimeLocal && dateSelection.value && dateSelection.value.end) { + const { year, month, day } = dateSelection.value.end + dateFilter = `${year}-${month}-${day}` + } else { + dateFilter = dateFirst.toISOString().substring(0, 10) + } + return dateFilter + } + + const getMonitoringVisitMappers = async () => { + isLoading.dataListLoading = true + data.value = [] + const dateFilter = getDateFilter() + const result = await geMonitoringVisitList({ + date: dateFilter || '', + serviceType: serviceType.value, + }) + + if (result && result.success && result.body) { + const visitsRaw = result.body?.response?.sep || [] + if (!visitsRaw) { + isLoading.dataListLoading = false + return + } + visitsRaw.forEach((result: any) => { + let st = result.jnsPelayanan || '-' + if (st === 'R.Inap') st = 'Rawat Inap' + else if (st === '1' || st === 'R.Jalan') st = 'Rawat Jalan' + + data.value.push({ + letterDate: result.tglSep || '-', + letterNumber: result.noSep || '-', + serviceType: st, + 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 + } + + const getSepList = async () => { + await getMonitoringVisitMappers() + } + + const setServiceTypes = () => { + serviceTypesList.value = Object.keys(serviceTypes).map((item) => ({ + value: item.toString(), + label: serviceTypes[item], + })) as any + } + + const setDateRange = () => { + const startCal = dateSelection.value.start + const endCal = dateSelection.value.end + const s = startCal ? startCal.toDate(getLocalTimeZone()) : today + const e = endCal ? endCal.toDate(getLocalTimeZone()) : today + dateRange.value = `${getFormatDateId(s)} - ${getFormatDateId(e)}` + } + + const handleExportCsv = () => { + if (!data.value || data.value.length === 0) { + toast({ title: 'Kosong', description: 'Tidak ada data untuk diekspor', variant: 'destructive' }) + return + } + + const yyyy = today.getFullYear() + const mm = String(today.getMonth() + 1).padStart(2, '0') + const dd = String(today.getDate()).padStart(2, '0') + const dateStr = `${yyyy}-${mm}-${dd}` + const filename = `file-sep-${dateStr}.csv` + downloadCsv(headerKeys, headerLabels, data.value, filename, ',', true) + } + + const handleExportExcel = async () => { + if (!data.value || data.value.length === 0) { + toast({ title: 'Kosong', description: 'Tidak ada data untuk diekspor', variant: 'destructive' }) + return + } + + const yyyy = today.getFullYear() + const mm = String(today.getMonth() + 1).padStart(2, '0') + const dd = String(today.getDate()).padStart(2, '0') + const dateStr = `${yyyy}-${mm}-${dd}` + const filename = `file-sep-${dateStr}.xlsx` + try { + await downloadXls(headerKeys, headerLabels, data.value, filename, 'SEP Data') + } catch (err: any) { + console.error('exportExcel error', err) + toast({ title: 'Gagal', description: err?.message || 'Gagal mengekspor data ke Excel', variant: 'destructive' }) + } + } + + const handleRowSelected = (row: any) => { + if (!row) return + sepData.value.sepNumber = row.letterNumber || '' + sepData.value.cardNumber = row.cardNumber || '' + sepData.value.patientName = row.patientName || '' + recItem.value = row + recId.value = (row && (row.id || row.recId)) || 0 + } + + const handlePageChange = (page: number) => { + console.log('pageChange', page) + } + + const handleRemove = async () => { + try { + const result = await removeSepData( + makeSepDataForRemove({ ...sepData.value, userName: userStore.user?.user_name }), + ) + const backendMessage = result?.body?.message || result?.message || null + const backendStatus = result?.body?.status || result?.status || null + + if ( + backendMessage === 'success' || + (backendStatus === 'error' && backendMessage === 'Decrypt failed: illegal base64 data at input byte 16') + ) { + await getSepList() + toast({ title: 'Berhasil', description: backendMessage || 'Data berhasil dihapus', variant: 'default' }) + } else { + toast({ title: 'Gagal', description: backendMessage || 'Gagal menghapus data', variant: 'destructive' }) + } + } catch (err: any) { + console.error('handleRemove error', err) + toast({ + title: 'Gagal', + description: err?.message || 'Terjadi kesalahan saat menghapus data', + variant: 'destructive', + }) + } finally { + recId.value = 0 + recAction.value = '' + open.value = false + } + } + + return { + recId, + recAction, + recItem, + data, + dateSelection, + dateRange, + serviceType, + serviceTypesList, + search, + open, + sepData, + headerPrep, + refSearchNav, + paginationMeta, + isLoading, + getSepList, + setServiceTypes, + setDateRange, + handleExportCsv, + handleExportExcel, + handleRowSelected, + handlePageChange, + handleRemove, + } +} + +export default useIntegrationSepList diff --git a/app/lib/constants.ts b/app/lib/constants.ts index 48fb5c8c..9cde7f73 100644 --- a/app/lib/constants.ts +++ b/app/lib/constants.ts @@ -8,7 +8,7 @@ export const dataStatusCodes: Record = { review: 'Review', process: 'Proses', done: 'Selesai', - canceled: 'Dibatalkan', + cancel: 'Dibatalkan', rejected: 'Ditolak', skiped: 'Dilewati', } diff --git a/app/lib/date.ts b/app/lib/date.ts index 2c7b92cf..982c3c5b 100644 --- a/app/lib/date.ts +++ b/app/lib/date.ts @@ -1,46 +1,69 @@ +const monthsInId = [ + 'Januari', + 'Februari', + 'Maret', + 'April', + 'Mei', + 'Juni', + 'Juli', + 'Agustus', + 'September', + 'Oktober', + 'November', + 'Desember', +] + export function getAge(dateString: string, comparedDate?: string): { idFormat: string; extFormat: string } { - const birthDate = new Date(dateString); - const today = new Date(); + const birthDate = new Date(dateString) + const today = new Date() - if (comparedDate) { - const comparedDateObj = new Date(comparedDate); - today.setFullYear(comparedDateObj.getFullYear()); - today.setMonth(comparedDateObj.getMonth()); - today.setDate(comparedDateObj.getDate()); - } + if (comparedDate) { + const comparedDateObj = new Date(comparedDate) + today.setFullYear(comparedDateObj.getFullYear()) + today.setMonth(comparedDateObj.getMonth()) + today.setDate(comparedDateObj.getDate()) + } - // Format the date part - const options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', year: 'numeric' }; - const idFormat = birthDate.toLocaleDateString('id-ID', options); + // Format the date part + const options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', year: 'numeric' } + const idFormat = birthDate.toLocaleDateString('id-ID', options) - // Calculate age - let years = today.getFullYear() - birthDate.getFullYear(); - let months = today.getMonth() - birthDate.getMonth(); - let days = today.getDate() - birthDate.getDate(); + // Calculate age + let years = today.getFullYear() - birthDate.getFullYear() + let months = today.getMonth() - birthDate.getMonth() + let days = today.getDate() - birthDate.getDate() - if (months < 0 || (months === 0 && days < 0)) { - years--; - months += 12; - } + if (months < 0 || (months === 0 && days < 0)) { + years-- + months += 12 + } - if (days < 0) { - const prevMonth = new Date(today.getFullYear(), today.getMonth() - 1, 0); - days += prevMonth.getDate(); - months--; - } + if (days < 0) { + const prevMonth = new Date(today.getFullYear(), today.getMonth() - 1, 0) + days += prevMonth.getDate() + months-- + } - // Format the age part - let extFormat = ''; - if ([years, months, days].filter(Boolean).join(' ')) { - extFormat = `${years} Tahun ${months} Bulan ${days} Hari`; - } else { - extFormat = '0'; - } + // Format the age part + let extFormat = '' + if ([years, months, days].filter(Boolean).join(' ')) { + extFormat = `${years} Tahun ${months} Bulan ${days} Hari` + } else { + extFormat = '0' + } - return { - idFormat, - extFormat - }; + return { + idFormat, + extFormat, + } +} + +// Date selection: default to today - today +export function getFormatDateId(date: Date) { + const dd = String(date.getDate()).padStart(2, '0') + const mm = monthsInId[date.getMonth()] + const yyyy = date.getFullYear() + return `${dd} ${mm} ${yyyy}` } export function formatDateYyyyMmDd(isoDateString: string): string { @@ -49,4 +72,17 @@ export function formatDateYyyyMmDd(isoDateString: string): string { const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; -} \ No newline at end of file +} + +// Function to check if date is invalid (like "0001-01-01T00:00:00Z") +export function isValidDate(dateString: string | null | undefined): boolean { + if (!dateString) return false + // Check for invalid date patterns + if (dateString.startsWith('0001-01-01')) return false + try { + const date = new Date(dateString) + return !isNaN(date.getTime()) + } catch { + return false + } +} diff --git a/app/lib/download.ts b/app/lib/download.ts new file mode 100644 index 00000000..eb996432 --- /dev/null +++ b/app/lib/download.ts @@ -0,0 +1,154 @@ +/** + * Download data as CSV file. + * + * @param headers - Array of header names. If omitted and data is array of objects, keys will be taken from first object. + * @param data - Array of rows. Each row can be either an object (key -> value) or an array of values. + * @param filename - optional file name to use for downloaded file + * @param delimiter - csv delimiter (default is comma) + * @param addBOM - add UTF-8 BOM to the file to make Excel detect UTF-8 correctly + * Usage examples: + * 1) With headers and array of objects + * downloadCsv(['name', 'age'], [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.csv'); + * 2) Without headers (automatically uses object keys) + * downloadCsv(null, [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.csv'); + * 3) With array-of-arrays + * downloadCsv(['col1', 'col2'], [['a', 'b'], ['c', 'd']], 'matrix.csv'); + */ +export function downloadCsv( + headers: string[] | null, + headerLabels: string[], + data: Array | any[]>, + filename = 'data.csv', + delimiter = ',', + addBOM = true, +) { + if (!Array.isArray(data) || data.length === 0) { + // still create an empty CSV containing only headers + const csvHeader = headers ? headers.join(delimiter) : '' + const csvString = addBOM ? '\uFEFF' + csvHeader : csvHeader + const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }) + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.setAttribute('download', filename) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + return + } + + // if headers not provided and rows are objects, take keys from first object + let _headers: string[] | null = headers + if (!_headers) { + const firstRow = data[0] + if (typeof firstRow === 'object' && !Array.isArray(firstRow)) { + _headers = Object.keys(firstRow) + } else if (Array.isArray(firstRow)) { + // if rows are arrays and no headers provided, we won't add header row + _headers = null + } + } + + const escape = (val: unknown) => { + if (val === null || typeof val === 'undefined') return '' + const str = String(val) + const needsQuoting = str.includes(delimiter) || str.includes('\n') || str.includes('\r') || str.includes('"') + if (!needsQuoting) return str + return '"' + str.replace(/"/g, '""') + '"' + } + + const rows: string[] = data.map((row) => { + if (Array.isArray(row)) { + return row.map(escape).join(delimiter) + } + // object row - map using headers if available, otherwise use object values + if (_headers && Array.isArray(_headers)) { + return _headers.map((h) => escape((row as Record)[h])).join(delimiter) + } + return Object.values(row).map(escape).join(delimiter) + }) + + const headerRow = headerLabels ? headerLabels.join(delimiter) : _headers ? _headers.join(delimiter) : null + const csvString = (addBOM ? '\uFEFF' : '') + [headerRow, ...rows].filter(Boolean).join('\r\n') + + const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }) + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.setAttribute('download', filename) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) +} + +/** + * Download data as XLS (Excel) file using xlsx library. + * + * @param headers - Array of header names. If omitted and data is array of objects, keys will be taken from first object. + * @param data - Array of rows. Each row can be either an object (key -> value) or an array of values. + * @param filename - optional file name to use for downloaded file (default: 'data.xlsx') + * @param sheetName - optional sheet name in workbook (default: 'Sheet1') + * Usage examples: + * 1) With headers and array of objects + * await downloadXls(['name', 'age'], [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.xlsx'); + * 2) Without headers (automatically uses object keys) + * await downloadXls(null, [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.xlsx'); + * 3) With custom sheet name + * await downloadXls(['col1', 'col2'], [['a', 'b'], ['c', 'd']], 'matrix.xlsx', 'MyData'); + */ +export async function downloadXls( + headers: string[] | null, + headerLabels: string[], + data: Array | any[]>, + filename = 'data.xlsx', + sheetName = 'Sheet1', +) { + // Dynamically import xlsx to avoid server-side issues + const { utils, write } = await import('xlsx') + const { saveAs } = await import('file-saver') + + if (!Array.isArray(data) || data.length === 0) { + // Create empty sheet with headers only + const ws = utils.aoa_to_sheet(headers ? [headers] : [[]]) + const wb = utils.book_new() + utils.book_append_sheet(wb, ws, sheetName) + const wbout = write(wb, { bookType: 'xlsx', type: 'array' }) + saveAs(new Blob([wbout], { type: 'application/octet-stream' }), filename) + return + } + + // if headers not provided and rows are objects, take keys from first object + let _headers: string[] | null = headers + if (!_headers) { + const firstRow = data[0] + if (typeof firstRow === 'object' && !Array.isArray(firstRow)) { + _headers = Object.keys(firstRow) + } else if (Array.isArray(firstRow)) { + _headers = null + } + } + + // Convert data rows to 2D array + const rows: any[][] = data.map((row) => { + if (Array.isArray(row)) { + return row + } + // object row - map using headers if available, otherwise use object values + if (_headers && Array.isArray(_headers)) { + return _headers.map((h) => (row as Record)[h] ?? '') + } + return Object.values(row) + }) + + // Combine headers/labels and rows for sheet + // If caller provided headerLabels (as display labels), prefer them. + const sheetHeader = headerLabels ? headerLabels : _headers ? _headers : null + const sheetData = sheetHeader ? [sheetHeader, ...rows] : rows + + // Create worksheet and workbook + const ws = utils.aoa_to_sheet(sheetData) + const wb = utils.book_new() + utils.book_append_sheet(wb, ws, sheetName) + + // Write and save file + const wbout = write(wb, { bookType: 'xlsx', type: 'array' }) + saveAs(new Blob([wbout], { type: 'application/octet-stream' }), filename) +} diff --git a/app/lib/roles.ts b/app/lib/roles.ts new file mode 100644 index 00000000..96d27780 --- /dev/null +++ b/app/lib/roles.ts @@ -0,0 +1,14 @@ +export const medicalPositions = ['emp|doc', 'emp|lab', 'emp|mid', 'emp|nur', 'emp|nut', 'emp|pha', 'emp|reg'] +const verificatorRole = 'verificator' + +export function getPositionAs(roleAccess: string): string { + if (roleAccess.includes('|')) { + if (medicalPositions.includes(roleAccess)) { + return 'medical' + } + if (roleAccess.includes(verificatorRole)) { + return 'verificator' + } + } + return 'none' +} diff --git a/app/models/encounter.ts b/app/models/encounter.ts index 55fbdfa4..85a0012d 100644 --- a/app/models/encounter.ts +++ b/app/models/encounter.ts @@ -37,6 +37,7 @@ export interface Encounter { discharge_date?: string internalReferences?: InternalReference[] deathCause?: DeathCause + paymentMethod_code?: string status_code: string encounterDocuments: EncounterDocument[] } diff --git a/app/pages/(features)/chemotherapy/encounter/[id]/detail.vue b/app/pages/(features)/chemotherapy/encounter/[id]/detail.vue new file mode 100644 index 00000000..534f49b3 --- /dev/null +++ b/app/pages/(features)/chemotherapy/encounter/[id]/detail.vue @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/app/pages/(features)/chemotherapy/encounter/[id]/process.vue b/app/pages/(features)/chemotherapy/encounter/[id]/process.vue new file mode 100644 index 00000000..030440e2 --- /dev/null +++ b/app/pages/(features)/chemotherapy/encounter/[id]/process.vue @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/app/pages/(features)/chemotherapy/encounter/add.vue b/app/pages/(features)/chemotherapy/encounter/add.vue new file mode 100644 index 00000000..438f0773 --- /dev/null +++ b/app/pages/(features)/chemotherapy/encounter/add.vue @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/app/pages/(features)/chemotherapy/encounter/index.vue b/app/pages/(features)/chemotherapy/encounter/index.vue new file mode 100644 index 00000000..4a8c9360 --- /dev/null +++ b/app/pages/(features)/chemotherapy/encounter/index.vue @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/app/pages/(features)/emergency/encounter/[id]/detail.vue b/app/pages/(features)/emergency/encounter/[id]/detail.vue new file mode 100644 index 00000000..77babf09 --- /dev/null +++ b/app/pages/(features)/emergency/encounter/[id]/detail.vue @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/app/pages/(features)/emergency/encounter/[id]/process.vue b/app/pages/(features)/emergency/encounter/[id]/process.vue new file mode 100644 index 00000000..7143d512 --- /dev/null +++ b/app/pages/(features)/emergency/encounter/[id]/process.vue @@ -0,0 +1,39 @@ + + + + + + + + \ No newline at end of file diff --git a/app/pages/(features)/inpatient/encounter/[id]/detail.vue b/app/pages/(features)/inpatient/encounter/[id]/detail.vue new file mode 100644 index 00000000..45751701 --- /dev/null +++ b/app/pages/(features)/inpatient/encounter/[id]/detail.vue @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/app/pages/(features)/inpatient/encounter/[id]/process.vue b/app/pages/(features)/inpatient/encounter/[id]/process.vue new file mode 100644 index 00000000..0a3fbe72 --- /dev/null +++ b/app/pages/(features)/inpatient/encounter/[id]/process.vue @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/app/pages/(features)/inpatient/encounter/index.vue b/app/pages/(features)/inpatient/encounter/index.vue index c40fec0b..3e12f65c 100644 --- a/app/pages/(features)/inpatient/encounter/index.vue +++ b/app/pages/(features)/inpatient/encounter/index.vue @@ -45,7 +45,7 @@ const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg' diff --git a/app/pages/(features)/integration/bpjs/control-letter/index.vue b/app/pages/(features)/integration/bpjs-vclaim/control-letter/index.vue similarity index 100% rename from app/pages/(features)/integration/bpjs/control-letter/index.vue rename to app/pages/(features)/integration/bpjs-vclaim/control-letter/index.vue diff --git a/app/pages/(features)/integration/bpjs/sep/add.vue b/app/pages/(features)/integration/bpjs-vclaim/sep/add.vue similarity index 100% rename from app/pages/(features)/integration/bpjs/sep/add.vue rename to app/pages/(features)/integration/bpjs-vclaim/sep/add.vue diff --git a/app/pages/(features)/integration/bpjs/sep/index.vue b/app/pages/(features)/integration/bpjs-vclaim/sep/index.vue similarity index 100% rename from app/pages/(features)/integration/bpjs/sep/index.vue rename to app/pages/(features)/integration/bpjs-vclaim/sep/index.vue diff --git a/app/pages/(features)/outpatient/encounter/[id]/detail.vue b/app/pages/(features)/outpatient/encounter/[id]/detail.vue new file mode 100644 index 00000000..534f49b3 --- /dev/null +++ b/app/pages/(features)/outpatient/encounter/[id]/detail.vue @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/app/pages/(features)/outpatient/encounter/[id]/process.vue b/app/pages/(features)/outpatient/encounter/[id]/process.vue index dfe36eed..108791e2 100644 --- a/app/pages/(features)/outpatient/encounter/[id]/process.vue +++ b/app/pages/(features)/outpatient/encounter/[id]/process.vue @@ -2,8 +2,7 @@ import type { Permission } from '~/models/role' import { permissions } from '~/const/page-permission/outpatient' import Error from '~/components/pub/my-ui/error/error.vue' - -import Content from '~/components/content/encounter/process.vue' +import Content from '~/components/content/encounter/process-next.vue' definePageMeta({ middleware: ['rbac'], @@ -30,12 +29,11 @@ const route = useRoute() useHead({ title: () => `${route.meta.title}`, }) - - - + + diff --git a/app/pages/(features)/outpatient/encounter/index.vue b/app/pages/(features)/outpatient/encounter/index.vue index 24520382..168203c5 100644 --- a/app/pages/(features)/outpatient/encounter/index.vue +++ b/app/pages/(features)/outpatient/encounter/index.vue @@ -46,6 +46,7 @@ const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg' +import type { PagePermission } from '~/models/role' +import Error from '~/components/pub/my-ui/error/error.vue' +import { PAGE_PERMISSIONS } from '~/lib/page-permission' +import EncounterProcess from '~/components/content/encounter/process-next.vue' + +definePageMeta({ + middleware: ['rbac'], + roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'], + title: 'Tambah Kunjungan', + contentFrame: 'cf-full-width', +}) + +const route = useRoute() + +useHead({ + title: () => `${route.meta.title}`, +}) + +const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter'] + +const { checkRole, hasCreateAccess, getPagePermissions } = 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 pagePermission = getPagePermissions(roleAccess) + + + + + + + + diff --git a/app/schemas/integration-bpjs.schema.ts b/app/schemas/integration-bpjs.schema.ts index 47206edf..452735f7 100644 --- a/app/schemas/integration-bpjs.schema.ts +++ b/app/schemas/integration-bpjs.schema.ts @@ -78,7 +78,7 @@ const IntegrationBpjsSchema = z .optional(), destinationClinic: z .string({ required_error: ERROR_MESSAGES.required.destinationClinic }) - .min(1, ERROR_MESSAGES.required.destinationClinic), + .min(1, ERROR_MESSAGES.required.destinationClinic).optional(), attendingDoctor: z .string({ required_error: ERROR_MESSAGES.required.attendingDoctor }) .min(1, ERROR_MESSAGES.required.attendingDoctor), @@ -89,7 +89,7 @@ const IntegrationBpjsSchema = z cataract: z.string({ required_error: ERROR_MESSAGES.required.cataract }).min(1, ERROR_MESSAGES.required.cataract), clinicExcecutive: z .string({ required_error: ERROR_MESSAGES.required.clinicExcecutive }) - .min(1, ERROR_MESSAGES.required.clinicExcecutive), + .min(1, ERROR_MESSAGES.required.clinicExcecutive).optional(), subSpecialistId: z .string({ required_error: ERROR_MESSAGES.required.subSpecialistId }) .min(1, ERROR_MESSAGES.required.subSpecialistId) diff --git a/app/services/_crud-base.ts b/app/services/_crud-base.ts index e3c57689..bf3a89c5 100644 --- a/app/services/_crud-base.ts +++ b/app/services/_crud-base.ts @@ -74,6 +74,19 @@ export async function update(path: string, id: number | string, data: any, name: } } +export async function updateCustom(path: string, data: any, name: string = 'item') { + try { + const resp = await xfetch(`${path}`, 'PATCH', data) + const result: any = {} + result.success = resp.success + result.body = (resp.body as Record) || {} + return result + } catch (error) { + console.error(`Error putting ${name}:`, error) + throw new Error(`Failed to put ${name}`) + } +} + export async function remove(path: string, id: number | string, name: string = 'item') { try { const resp = await xfetch(`${path}/${id}`, 'DELETE') @@ -86,3 +99,16 @@ export async function remove(path: string, id: number | string, name: string = ' throw new Error(`Failed to delete ${name}`) } } + +export async function removeCustom(path: string, data: any, name: string = 'item') { + try { + const resp = await xfetch(`${path}`, 'DELETE', data) + const result: any = {} + result.success = resp.success + result.body = (resp.body as Record) || {} + return result + } catch (error) { + console.error(`Error deleting ${name}:`, error) + throw new Error(`Failed to delete ${name}`) + } +} \ No newline at end of file diff --git a/app/services/encounter.service.ts b/app/services/encounter.service.ts index 6e42fd41..3643277a 100644 --- a/app/services/encounter.service.ts +++ b/app/services/encounter.service.ts @@ -28,6 +28,11 @@ export function remove(id: number | string) { return base.remove(path, id, name) } +export function cancel(id: number | string) { + let url = `${path}/${id}/cancel` + return base.updateCustom(url, null, name) +} + export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> { let data: { value: string; label: string }[] = [] const result = await getList(params) diff --git a/app/services/vclaim-control-letter.service.ts b/app/services/vclaim-control-letter.service.ts index 007e91c5..2c638c2e 100644 --- a/app/services/vclaim-control-letter.service.ts +++ b/app/services/vclaim-control-letter.service.ts @@ -15,11 +15,11 @@ export function getList(params: any = null) { if (params?.letterNumber && params.mode === 'by-sep') { url += `/${params.letterNumber}` } - if (params?.letterNumber && params.mode === 'by-schedule') { - url += `/jadwalDokter?jeniskontrol=${params.controlType}&kodepoli=${params.poliCode}&tanggalkontrol=${params.controlDate}` + if (params?.controlDate && params.mode === 'by-schedule') { + url += `/jadwalDokter?jeniskontrol=${params.controlType}&kodepoli=${params.polyCode}&tanggalkontrol=${params.controlDate}` delete params.controlType - delete params.poliCode delete params.controlDate + delete params.polyCode } if (params) { delete params.letterNumber diff --git a/app/services/vclaim-monitoring-visit.service.ts b/app/services/vclaim-monitoring-visit.service.ts index 0c5da64e..6004673e 100644 --- a/app/services/vclaim-monitoring-visit.service.ts +++ b/app/services/vclaim-monitoring-visit.service.ts @@ -4,68 +4,16 @@ import * as base from './_crud-base' const path = '/api/vclaim/v1/monitoring/visit' const name = 'monitoring-visit' -const dummyResponse = { - metaData: { - code: '200', - message: 'Sukses', - }, - response: { - sep: [ - { - diagnosa: 'K65.0', - jnsPelayanan: 'R.Inap', - kelasRawat: '2', - nama: 'HANIF ABDURRAHMAN', - noKartu: '0001819122189', - noSep: '0301R00110170000004', - noRujukan: '0301U01108180200084', - poli: null, - tglPlgSep: '2017-10-03', - tglSep: '2017-10-01', - }, - { - diagnosa: 'I50.0', - jnsPelayanan: 'R.Inap', - kelasRawat: '3', - nama: 'ASRIZAL', - noKartu: '0002283324674', - noSep: '0301R00110170000005', - noRujukan: '0301U01108180200184', - poli: null, - tglPlgSep: '2017-10-10', - tglSep: '2017-10-01', - }, - ], - }, -} - export async function getList(params: any = null) { - try { - let url = path - if (params?.date && params.serviceType) { - url += `/${params.date}/${params.serviceType}` - } - if (params) { - delete params.date - delete params.serviceType - } - const resp = await base.getList(url, params, name) - - // Jika success false, return dummy response - if (!resp.success || !resp.body?.response) { - return { - success: true, - body: dummyResponse, - } - } - - return resp - } catch (error) { - // Jika terjadi error, return dummy response - console.error(`Error fetching ${name}s:`, error) - return { - success: true, - body: dummyResponse, - } + let url = path + if (params?.date && params.serviceType) { + url += `/${params.date}/${params.serviceType}` } + if (params) { + delete params.date + delete params.serviceType + } + const resp = await base.getList(url, params, name) + + return resp } diff --git a/app/services/vclaim-sep.service.ts b/app/services/vclaim-sep.service.ts index fdccc9c4..93224a4a 100644 --- a/app/services/vclaim-sep.service.ts +++ b/app/services/vclaim-sep.service.ts @@ -7,6 +7,9 @@ import type { IntegrationBpjsFormData } from '~/schemas/integration-bpjs.schema' const path = '/api/vclaim-swagger/sep' const name = 'sep' +// TODO: temporary destinationClinic +const destinationClinic = '1323R001' + export function create(data: any) { return base.create(path, data, name) } @@ -20,8 +23,19 @@ export function getList(params: any = null) { return base.getList(url, params, name) } +export function getDetail(id: number | string) { + return base.getDetail(path, id, name) +} + +export function remove(payload: any) { + const url = `${path}` + return base.removeCustom(url, payload, name) +} + export function makeSepData( data: IntegrationBpjsFormData & { + userName: string + polyCode?: string referralFrom?: string referralTo?: string referralLetterDate?: string @@ -31,13 +45,13 @@ export function makeSepData( const content = { noKartu: data.cardNumber || '', tglSep: data.sepDate, - ppkPelayanan: data.fromClinic || '', - jnsPelayanan: data.admissionType ? String(data.admissionType) : '1', + ppkPelayanan: destinationClinic || data.fromClinic || '', + jnsPelayanan: data.serviceType ? String(data.serviceType) : '2', noMR: data.medicalRecordNumber || '', catatan: data.note || '', diagAwal: data.initialDiagnosis || '', poli: { - tujuan: data.destinationClinic || '', + tujuan: data.polyCode || '', eksekutif: data.clinicExcecutive === 'yes' ? '1' : '0', }, cob: { @@ -46,19 +60,21 @@ export function makeSepData( katarak: { katarak: data.cataract === 'yes' ? '1' : '0', }, - tujuanKunj: data.purposeOfVisit || '', + tujuanKunj: data.purposeOfVisit || '0', flagProcedure: data.procedureType || '', kdPenunjang: data.supportCode || '', assesmentPel: data.serviceAssessment || '', skdp: { noSurat: ['3'].includes(data.admissionType) ? data.referralLetterNumber : '', - kodeDPJP: ['3'].includes(data.admissionType)? data.attendingDoctor : '', + kodeDPJP: ['3'].includes(data.admissionType) ? data.attendingDoctor : '', }, rujukan: { - asalRujukan: ['2'].includes(data.admissionType) ? data?.referralFrom || '' : '', - tglRujukan: ['2'].includes(data.admissionType) ? data?.referralLetterDate || '' : '', - noRujukan: ['2'].includes(data.admissionType) ? data?.referralLetterNumber || '' : '', - ppkRujukan: ['2'].includes(data.admissionType) ? data?.referralTo || '' : '', + // Handle referral data for admissionType !== '3' + // asalRujukan: 1 = Faskes 1, 2 = Faskes RS + asalRujukan: !['3'].includes(data.admissionType) ? data?.referralFrom || '' : '', + tglRujukan: !['3'].includes(data.admissionType) ? data?.referralLetterDate || '' : '', + noRujukan: !['3'].includes(data.admissionType) ? data?.referralLetterNumber || '' : '', + ppkRujukan: !['3'].includes(data.admissionType) ? data?.referralTo || '' : '', }, klsRawat: { klsRawatHak: data.classLevel || '', @@ -68,7 +84,7 @@ export function makeSepData( }, dpjpLayan: data.attendingDoctor || '', noTelp: data.phoneNumber || '', - user: data.patientName || '', + user: data.userName || '', jaminan: { lakaLantas: data.trafficAccident || '0', noLP: data.lpNumber || '', @@ -93,3 +109,14 @@ export function makeSepData( }, } } + +export function makeSepDataForRemove(data: any) { + return { + request: { + t_sep: { + noSep: data.sepNumber, + user: data.userName, + }, + }, + } +} diff --git a/package.json b/package.json index d415abde..08b0d87c 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,12 @@ "date-fns": "^4.1.0", "embla-carousel": "^8.5.2", "embla-carousel-vue": "^8.5.2", + "file-saver": "^2.0.5", "h3": "^1.15.4", "pinia": "^3.0.3", "pinia-plugin-persistedstate": "^4.4.1", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "xlsx": "^0.18.5" }, "devDependencies": { "@antfu/eslint-config": "^4.10.1", @@ -36,6 +38,7 @@ "@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/tailwindcss": "6.14.0", "@pinia/nuxt": "^0.11.2", + "@types/file-saver": "^2.0.7", "@unocss/eslint-plugin": "^66.0.0", "@unocss/nuxt": "^66.0.0", "@vee-validate/zod": "^4.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ae777c3..565ff313 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: embla-carousel-vue: specifier: ^8.5.2 version: 8.6.0(vue@3.5.21) + file-saver: + specifier: ^2.0.5 + version: 2.0.5 h3: specifier: ^1.15.4 version: 1.15.4 @@ -44,6 +47,9 @@ dependencies: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.17) + xlsx: + specifier: ^0.18.5 + version: 0.18.5 devDependencies: '@antfu/eslint-config': @@ -67,6 +73,9 @@ devDependencies: '@pinia/nuxt': specifier: ^0.11.2 version: 0.11.2(pinia@3.0.3) + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 '@unocss/eslint-plugin': specifier: ^66.0.0 version: 66.5.1(eslint@9.36.0)(typescript@5.9.2) @@ -3182,6 +3191,10 @@ packages: /@types/estree@1.0.8: resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + /@types/file-saver@2.0.7: + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + dev: true + /@types/geojson@7946.0.16: resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} dev: false @@ -4588,6 +4601,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + /adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + dev: false + /agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -4796,12 +4814,12 @@ packages: dev: true optional: true - /my-ui64-js@1.5.1: + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: true - /my-uiline-browser-mapping@2.8.6: - resolution: {integrity: sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==} + /baseline-browser-mapping@2.8.29: + resolution: {integrity: sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==} hasBin: true dev: true @@ -4845,7 +4863,7 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - baseline-browser-mapping: 2.8.6 + baseline-browser-mapping: 2.8.29 caniuse-lite: 1.0.30001743 electron-to-chromium: 1.5.222 node-releases: 2.0.21 @@ -4966,6 +4984,14 @@ packages: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} dev: true + /cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + dev: false + /chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -5084,6 +5110,11 @@ packages: engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dev: true + /codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + dev: false + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -5244,7 +5275,6 @@ packages: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} hasBin: true - dev: true /crc32-stream@6.0.0: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} @@ -6754,6 +6784,10 @@ packages: flat-cache: 4.0.1 dev: true + /file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + dev: false + /file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} dev: true @@ -6814,6 +6848,11 @@ packages: engines: {node: '>=0.4.x'} dev: true + /frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + dev: false + /fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} dev: true @@ -10219,6 +10258,13 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + /ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + dependencies: + frac: 1.1.2 + dev: false + /stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -11603,11 +11649,21 @@ packages: stackback: 0.0.2 dev: true + /wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + dev: false + /word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} dev: true + /word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + dev: false + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -11648,6 +11704,20 @@ packages: is-wsl: 3.1.0 dev: true + /xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + dev: false + /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} diff --git a/public/bpjs.png b/public/bpjs.png new file mode 100644 index 00000000..781e19b0 Binary files /dev/null and b/public/bpjs.png differ diff --git a/public/side-menu-items/sys.json b/public/side-menu-items/sys.json index d1cdc98e..b3d86111 100644 --- a/public/side-menu-items/sys.json +++ b/public/side-menu-items/sys.json @@ -171,17 +171,17 @@ { "title": "SEP", "icon": "i-lucide-circuit-board", - "link": "/integration/bpjs/sep" + "link": "/integration/bpjs-vclaim/sep" }, { "title": "Peserta", "icon": "i-lucide-circuit-board", - "link": "/integration/bpjs/member" + "link": "/integration/bpjs-vclaim/member" }, { "title": "Surat Kontrol", "icon": "i-lucide-circuit-board", - "link": "/integration/bpjs/control-letter" + "link": "/integration/bpjs-vclaim/control-letter" } ] }, diff --git a/public/side-menu-items/system.json b/public/side-menu-items/system.json index e12f58ec..2dc94443 100644 --- a/public/side-menu-items/system.json +++ b/public/side-menu-items/system.json @@ -193,17 +193,17 @@ { "title": "SEP", "icon": "i-lucide-circuit-board", - "link": "/integration/bpjs/sep" + "link": "/integration/bpjs-vclaim/sep" }, { "title": "Peserta", "icon": "i-lucide-circuit-board", - "link": "/integration/bpjs/member" + "link": "/integration/bpjs-vclaim/member" }, { "title": "Surat Kontrol", "icon": "i-lucide-circuit-board", - "link": "/integration/bpjs/control-letter" + "link": "/integration/bpjs-vclaim/control-letter" } ] }, diff --git a/server/api/[...req].ts b/server/api/[...req].ts index 5948eda6..c58bf7d6 100644 --- a/server/api/[...req].ts +++ b/server/api/[...req].ts @@ -1,7 +1,7 @@ import { defineEventHandler, getCookie, getRequestHeaders, getRequestURL, readBody } from 'h3' export default defineEventHandler(async (event) => { - const { method } = event.node.req + const { method } = event.node.req as any const headers = getRequestHeaders(event) const url = getRequestURL(event) const config = useRuntimeConfig() @@ -36,7 +36,7 @@ export default defineEventHandler(async (event) => { } let body: any - if (['POST', 'PATCH'].includes(method!)) { + if (['POST', 'PATCH', 'PUT', 'DELETE'].includes(method)) { if (headers['content-type']?.includes('multipart/form-data')) { body = await readBody(event) } else {
+ {{ data.patient.number || '-' }} +
+ {{ birthDateFormatted }} +
+ {{ paymentTypeLabel }} +
+ {{ bedNumber }} +
+ {{ data.patient.person.name || '-' }} +
+ {{ registeredDateFormatted }} +
+ {{ billingNumber }} +
+ {{ dpjp }} +
+ {{ address }} +
+ {{ genderLabel }} +
+ {{ roomName }} +
+ Nama: + {{ record.patient.person.name }} +
+ No RM: + {{ record.medical_record_number }} +
No. SEP : {{ sepData.no_sep }}
No. Kartu BPJS : {{ sepData.kartu }}
Nama Pasien : {{ sepData.nama }}
No. SEP: {{ sepData.sepNumber }}
No. Kartu BPJS: {{ sepData.cardNumber }}
Nama Pasien: {{ sepData.patientName }}