From 53b40ec732948e856c4a40881c84787e22586979 Mon Sep 17 00:00:00 2001 From: riefive Date: Mon, 8 Dec 2025 13:02:14 +0700 Subject: [PATCH 01/15] feat: Implement patient encounter management with entry form, SEP integration, and list views. --- app/components/app/encounter/entry-form.vue | 1 + app/components/app/encounter/list.cfg.ts | 150 ++++++++++++++++-- app/components/app/encounter/list.vue | 7 +- .../app/encounter/vclaim-sep-info.vue | 33 ++++ app/components/content/encounter/list.vue | 22 ++- app/handlers/encounter-entry.handler.ts | 4 +- 6 files changed, 195 insertions(+), 22 deletions(-) create mode 100644 app/components/app/encounter/vclaim-sep-info.vue diff --git a/app/components/app/encounter/entry-form.vue b/app/components/app/encounter/entry-form.vue index 46ad2db7..56e0383b 100644 --- a/app/components/app/encounter/entry-form.vue +++ b/app/components/app/encounter/entry-form.vue @@ -187,6 +187,7 @@ function onAddSep() { registerDate: registerDate.value, cardNumber: cardNumber.value, paymentMethodCode: paymentMethodCode.value, + unitCode: props.selectedDoctor?.unit?.code || '', sepFile: sepFile.value, sippFile: sippFile.value, sepType: sepType.value, diff --git a/app/components/app/encounter/list.cfg.ts b/app/components/app/encounter/list.cfg.ts index cc0b951c..442233cc 100644 --- a/app/components/app/encounter/list.cfg.ts +++ b/app/components/app/encounter/list.cfg.ts @@ -1,25 +1,18 @@ import type { Config, RecComponent } from '~/components/pub/my-ui/data-table' import { defineAsyncComponent } from 'vue' import type { Encounter } from '~/models/encounter' -import { educationCodes, genderCodes } from '~/lib/constants' +import { formatAddress } from '~/models/person-address' +import { educationCodes, encounterClassCodes, genderCodes } from '~/lib/constants' import { getAge } from '~/lib/date' type SmallDetailDto = Encounter const action = defineAsyncComponent(() => import('./dropdown-action.vue')) const statusBadge = defineAsyncComponent(() => import('./status-badge.vue')) +const vclaimSepInfo = defineAsyncComponent(() => import('./vclaim-sep-info.vue')) -export const config: Config = { - cols: [ - {}, - {}, - {}, - { width: 160 }, - {}, - { width: 70 }, - { }, - { width: 50 }, - ], +export const defaultConfig: Config = { + cols: [{}, {}, {}, { width: 160 }, {}, { width: 70 }, {}, { width: 50 }], headers: [ [ @@ -94,11 +87,140 @@ export const config: Config = { birth_date: (rec: unknown): unknown => { const recX = rec as Encounter if (recX.patient?.person?.birthDate) { - return '' + - '
' + (recX.patient.person.birthDate as string).substring(0, 10) + ' /
' + + return ( + '' + + '
' + + (recX.patient.person.birthDate as string).substring(0, 10) + + ' /
' + getAge(recX.patient.person.birthDate as string).extFormat + ) } return '-' }, }, } + +export const ambulatoryConfig: Config = { + cols: [{}, {}, {}, { width: 160 }, {}, { width: 70 }, {}, {}, {}, {}, {}, {}, { width: 50 }], + + headers: [ + [ + { label: 'TANGGAL' }, + { label: 'NO. RM' }, + { label: 'NO. BILL' }, + { label: 'NAMA PASIEN' }, + { label: 'L/P' }, + { label: 'ALAMAT' }, + { label: 'KLINIK' }, + { label: 'CARA BAYAR' }, + { label: 'RUJUKAN' }, + { label: 'KET. RUJUKAN' }, + { label: 'ASAL' }, + { label: 'SEP' }, + { label: 'STATUS', classVal: '!text-center' }, + { label: '' }, + ], + ], + + keys: [ + 'registeredAt', + 'patientNumber', + 'trxNumber', + 'patient.person.name', + 'gender', + 'address', + 'clinic', + 'paymentMethod_code', + 'referral', + 'note', + 'class_code', + 'sep', + 'status', + 'action', + ], + + delKeyNames: [ + { key: 'code', label: 'Kode' }, + { key: 'name', label: 'Nama' }, + ], + + parses: { + registeredAt: (rec: unknown): unknown => { + const recX = rec as Encounter + return recX.registeredAt ? (recX.registeredAt as string).substring(0, 10) : '-' + }, + patientNumber: (rec: unknown): unknown => { + const recX = rec as any + return recX.patient?.number || '-' + }, + trxNumber: (rec: unknown): unknown => { + const recX = rec as any + return recX.trx_number || '-' + }, + gender: (rec: unknown): unknown => { + const recX = rec as Encounter + if (recX.patient?.person?.gender_code) { + return genderCodes[recX.patient.person.gender_code] + } + return '-' + }, + address: (rec: unknown): unknown => { + const recX = rec as Encounter + const addresses = recX.patient.person.addresses + const resident = addresses?.find((addr) => addr.locationType_code === 'domicile') + const text = resident ? formatAddress(resident) : '-' + return text.length > 20 ? text.substring(0, 17) + '...' : text + }, + clinic: (rec: unknown): unknown => { + const recX = rec as Encounter + return recX.unit?.name || recX.refSource_name || '-' + }, + paymentMethod_code: (rec: unknown): unknown => { + const recX = rec as Encounter + return (recX.paymentMethod_code || '-').toUpperCase() + }, + referral: (rec: unknown): unknown => { + const recX = rec as any + return recX?.vclaimReference?.poliRujukan || '-' + }, + note: (rec: unknown): unknown => { + const recX = rec as any + return recX?.vclaimReference?.ppkDirujuk || '-' + }, + class_code: (rec: unknown): unknown => { + const recX = rec as Encounter + return recX.class_code ? encounterClassCodes[recX.class_code] : '-' + }, + }, + + components: { + sep(rec, idx) { + const res: RecComponent = { + rec: rec as object, + idx, + component: vclaimSepInfo, + } + return res + }, + status(rec, idx) { + const recX = rec as Encounter + if (!recX.status_code) { + recX.status_code = 'new' + } + const res: RecComponent = { + idx, + rec: recX, + component: statusBadge, + } + return res + }, + action(rec, idx) { + const res: RecComponent = { + idx, + rec: rec as object, + component: action, + } + return res + }, + }, +} diff --git a/app/components/app/encounter/list.vue b/app/components/app/encounter/list.vue index 801e5b4c..0f171d4e 100644 --- a/app/components/app/encounter/list.vue +++ b/app/components/app/encounter/list.vue @@ -1,15 +1,16 @@ diff --git a/app/components/app/encounter/vclaim-sep-info.vue b/app/components/app/encounter/vclaim-sep-info.vue new file mode 100644 index 00000000..a72e1352 --- /dev/null +++ b/app/components/app/encounter/vclaim-sep-info.vue @@ -0,0 +1,33 @@ + + + diff --git a/app/components/content/encounter/list.vue b/app/components/content/encounter/list.vue index 3aeb1254..ebb49042 100644 --- a/app/components/content/encounter/list.vue +++ b/app/components/content/encounter/list.vue @@ -136,8 +136,21 @@ onMounted(() => { /////// Functions async function getPatientList() { isLoading.isTableLoading = true - const includesParams = - '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 includesParamsArrays = [ + 'patient', + 'patient-person', + 'patient-person-addresses', + 'Appointment_Doctor', + 'Appointment_Doctor-employee', + 'Appointment_Doctor-employee-person', + 'Responsible_Doctor', + 'Responsible_Doctor-employee', + 'Responsible_Doctor-employee-person', + 'EncounterDocuments', + 'unit', + 'vclaimReference', // vclaimReference | vclaimSep + ] + const includesParams = includesParamsArrays.join(',') data.value = [] try { const params: any = { includes: includesParams, ...filterParams.value } @@ -270,7 +283,10 @@ function handleRemoveConfirmation() { /> - + Date: Mon, 8 Dec 2025 14:02:41 +0700 Subject: [PATCH 02/15] feat: Implement encounter list views with new configurations, vclaim SEP/SIPP document handling, and initial encounter entry forms. --- ...form.vue.backup => entry-form-prev-00.vue} | 6 +- ...y-form-prev.vue => entry-form-prev-01.vue} | 0 app/components/app/encounter/list.cfg.ts | 6 +- .../app/encounter/vclaim-sep-info.vue | 52 ++++++++++-- app/components/content/encounter/list.vue | 85 +++++++++++++++++++ app/handlers/supporting-document.handler.ts | 1 + 6 files changed, 139 insertions(+), 11 deletions(-) rename app/components/app/encounter/{entry-form.vue.backup => entry-form-prev-00.vue} (99%) rename app/components/app/encounter/{entry-form-prev.vue => entry-form-prev-01.vue} (100%) diff --git a/app/components/app/encounter/entry-form.vue.backup b/app/components/app/encounter/entry-form-prev-00.vue similarity index 99% rename from app/components/app/encounter/entry-form.vue.backup rename to app/components/app/encounter/entry-form-prev-00.vue index f92b7736..a0c1ec28 100644 --- a/app/components/app/encounter/entry-form.vue.backup +++ b/app/components/app/encounter/entry-form-prev-00.vue @@ -43,7 +43,7 @@ const emit = defineEmits<{ }>() // Validation schema -const { handleSubmit, errors, defineField, meta } = useForm({ +const { handleSubmit, errors, defineField, meta } = useForm({ validationSchema: toTypedSchema(IntegrationEncounterSchema), }) @@ -141,7 +141,7 @@ function onAddSep() { registerDate: registerDate.value, cardNumber: cardNumber.value, paymentType: paymentType.value, - sepType: sepType.value + sepType: sepType.value, } emit('event', 'add-sep', formValues) } @@ -454,7 +454,7 @@ defineExpose({ name="i-lucide-loader-2" class="h-4 w-4 animate-spin" /> - { const recX = rec as Encounter - return recX.registeredAt ? (recX.registeredAt as string).substring(0, 10) : '-' + const currentDate = recX.registeredAt ? (recX.registeredAt as string) : (recX as any).createdAt + return currentDate ? currentDate.substring(0, 10) : '-' }, patientNumber: (rec: unknown): unknown => { const recX = rec as any @@ -194,10 +195,9 @@ export const ambulatoryConfig: Config = { }, components: { - sep(rec, idx) { + sep(rec) { const res: RecComponent = { rec: rec as object, - idx, component: vclaimSepInfo, } return res diff --git a/app/components/app/encounter/vclaim-sep-info.vue b/app/components/app/encounter/vclaim-sep-info.vue index a72e1352..06af4f85 100644 --- a/app/components/app/encounter/vclaim-sep-info.vue +++ b/app/components/app/encounter/vclaim-sep-info.vue @@ -1,33 +1,75 @@ diff --git a/app/components/content/encounter/list.vue b/app/components/content/encounter/list.vue index ebb49042..5cff930a 100644 --- a/app/components/content/encounter/list.vue +++ b/app/components/content/encounter/list.vue @@ -7,6 +7,7 @@ import { ActionEvents } from '~/components/pub/my-ui/data/types' import Dialog from '~/components/pub/my-ui/modal/dialog.vue' import * as CH from '~/components/pub/my-ui/content-header' import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue' +import FileUpload from '~/components/pub/my-ui/form/file-field.vue' import { useSidebar } from '~/components/pub/ui/sidebar/utils' // App libs @@ -19,6 +20,9 @@ import { cancel as cancelEncounter, } from '~/services/encounter.service' +// Handlers +import { uploadAttachmentCustom } from '~/handlers/supporting-document.handler' + // Apps import Content from '~/components/app/encounter/list.vue' import FilterNav from '~/components/app/encounter/filter-nav.vue' @@ -53,9 +57,13 @@ const isLoading = reactive({ const recId = ref(0) const recAction = ref('') const recItem = ref(null) +const recSepId = ref(0) +const recSepMenu = ref('') +const recSepSubMenu = ref('') const isFilterFormDialogOpen = ref(false) const isRecordConfirmationOpen = ref(false) const isRecordCancelOpen = ref(false) +const uploadFile = ref(null) // Headers const hreaderPrep: CH.Config = { @@ -102,6 +110,9 @@ provide('activeServicePosition', activeServicePosition) provide('rec_id', recId) provide('rec_action', recAction) provide('rec_item', recItem) +provide('rec_sep_id', recSepId) +provide('rec_sep_menu', recSepMenu) +provide('rec_sep_sub_menu', recSepSubMenu) provide('table_data_loader', isLoading) watch(getActiveRole, (role?: string) => { @@ -129,6 +140,19 @@ watch( }, ) +watch([recSepId, recSepMenu, recSepSubMenu], (value) => { + const id = value[0] + const menu = value[1] + const subMenu = value[2] + if (!id) return + if (subMenu === 'view') { + handleViewFile(id, menu, subMenu) + } + if (subMenu === 'edit') { + handleUploadFile(id, menu) + } +}) + onMounted(() => { getPatientList() }) @@ -172,6 +196,55 @@ async function getPatientList() { } } +function handleUploadFile(id: number, menu: string) { + uploadFile.value = null + document.getElementById('uploadFile')?.click() +} + +async function handleUploadFileSubmit() { + if (!uploadFile.value) return + const result = await uploadAttachmentCustom({ + file: uploadFile.value, + refId: recSepId.value, + entityTypeCode: 'encounter', + type: recSepMenu.value === 'sep' ? 'vclaim-sep' : 'vclaim-sipp', + }) + if (result.success) { + toast({ + title: 'Berhasil', + description: 'File berhasil diunggah', + variant: 'default', + }) + await getPatientList() + } else { + toast({ + title: 'Gagal', + description: 'File gagal diunggah', + variant: 'destructive', + }) + } +} + +function handleViewFile(id: number, menu: string, subMenu: string) { + const currentData: any = data.value.find((item: any) => item.id === id) + if (!currentData) return + let fileReviewSep: any = null + let fileReviewSipp: any = null + for (const doc of currentData.encounterDocuments) { + if (doc.type_code === 'vclaim-sep') { + fileReviewSep = { id: doc.id, fileName: doc.fileName, filePath: doc.filePath, type: doc.type_code } + } else if (doc.type_code === 'vclaim-sipp') { + fileReviewSipp = { id: doc.id, fileName: doc.fileName, filePath: doc.filePath, type: doc.type_code } + } + } + if (fileReviewSep && menu === 'sep' && subMenu === 'view') { + window.open(fileReviewSep.filePath, '_blank') + } + if (fileReviewSipp && menu === 'sipp' && subMenu === 'view') { + window.open(fileReviewSipp.filePath, '_blank') + } +} + function handleFilterApply(filters: { personName: string; startDate: string; endDate: string }) { filterParams.value = { 'person-name': filters.personName, @@ -356,4 +429,16 @@ function handleRemoveConfirmation() { > Hak akses tidak memenuhi kriteria untuk proses ini. + +