Merge pull request #212 from dikstub-rssa/feat/encounter-adjustment-163

Enhancement SEP
This commit is contained in:
Munawwirul Jamal
2025-12-04 14:30:35 +07:00
committed by GitHub
7 changed files with 203 additions and 98 deletions
+56 -22
View File
@@ -23,6 +23,7 @@ import { paymentMethodCodes } from '~/const/key-val/common'
// App things
import { genEncounter, type Encounter } from '~/models/encounter'
import { se } from 'date-fns/locale'
// Props
const props = defineProps<{
@@ -48,7 +49,7 @@ const model = defineModel<Encounter>()
model.value = genEncounter()
// Common preparation
const defaultCBItems = [{ label: 'Pilih', value: '' }];
const defaultCBItems = [{ label: 'Pilih', value: '' }]
const paymentMethodItems = CB.recStrToItem(paymentMethodCodes)
// Emit preparation
@@ -95,7 +96,10 @@ const isInsurancePayment = computed(() => ['insurance', 'jkn'].includes(paymentM
const isDateLoading = ref(false)
const debouncedSepNumber = refDebounced(sepNumber, 500)
const debouncedCardNumber = refDebounced(cardNumber, 500)
const sepFileReview = ref<any>(null)
const sippFileReview = ref<any>(null)
const unitFullName = ref('') // Unit, specialist, subspecialist
const formRef = ref<HTMLFormElement | null>(null) // Expose submit method for parent component
if (mode === 'add') {
// Set default sepDate to current date in YYYY-MM-DD format
@@ -106,11 +110,10 @@ if (mode === 'add') {
registerDate.value = `${year}-${month}-${day}`
}
watch(() => props.selectedDoctor, (doctor) => {
unitFullName.value = doctor.subspecialist?.name ??
doctor.specialist?.name ??
doctor.unit?.name ??
'tidak diketahui'
watch(
() => props.selectedDoctor,
(doctor) => {
unitFullName.value = doctor.subspecialist?.name ?? doctor.specialist?.name ?? doctor.unit?.name ?? 'tidak diketahui'
model.value!.unit_code = doctor.unit_code || ''
model.value!.specialist_code = doctor.specialist_code || ''
model.value!.subspecialist_code = doctor.subspecialist_code || ''
@@ -126,12 +129,13 @@ watch(
nationalIdentity.value = objects?.nationalIdentity || ''
medicalRecordNumber.value = objects?.medicalRecordNumber || ''
doctorCode.value = objects?.doctorCode || ''
// subSpecialistCode.value = objects?.subSpecialistCode || ''
paymentMethodCode.value = objects?.paymentMethodCode || ''
patientCategory.value = objects?.patientCategory || ''
cardNumber.value = objects?.cardNumber || ''
sepType.value = objects?.sepType || ''
sepNumber.value = objects?.sepNumber || ''
sepFileReview.value = objects?.sepFileReview || ''
sippFileReview.value = objects?.sippFileReview || ''
isDateLoading.value = true
setTimeout(() => {
registerDate.value = objects?.registerDate || ''
@@ -181,11 +185,12 @@ function onAddSep() {
const formValues = {
patientId: patientId.value || '',
doctorCode: doctorCode.value,
// subSpecialistCode: subSpecialistCode.value,
registerDate: registerDate.value,
cardNumber: cardNumber.value,
paymentMethodCode: paymentMethodCode.value,
sepType: sepType.value
sepFile: sepFile.value,
sippFile: sippFile.value,
sepType: sepType.value,
}
emit('event', 'add-sep', formValues)
}
@@ -196,12 +201,20 @@ function onSearchSep() {
// Submit handler
const onSubmit = handleSubmit((values) => {
console.log('✅ Validated form values:', JSON.stringify(values, null, 2))
emit('event', 'save', values)
let payload: any = values
if (props.mode === 'edit') {
payload = {
...payload,
sepFileReview: sepFileReview.value,
sippFileReview: sippFileReview.value,
}
}
emit('event', 'save', payload)
})
// Expose submit method for parent component
const formRef = ref<HTMLFormElement | null>(null)
function openFile(path: string) {
window.open(path, '_blank')
}
function submitForm() {
// Trigger form submit using native form submit
@@ -346,7 +359,10 @@ defineExpose({
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.unit_code">
<Input :value="unitFullName" :disabled="true"/>
<Input
:value="unitFullName"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
@@ -414,7 +430,7 @@ defineExpose({
placeholder="Pilih Kelompok Peserta"
/>
</DE.Field>
<span class="text-sm text-gray-500">
<span class="text-sm text-gray-500">
{{ noteReference }}
</span>
</DE.Cell>
@@ -433,7 +449,7 @@ defineExpose({
placeholder="Masukkan nomor kartu BPJS"
/>
</DE.Field>
<div
<div
v-if="isMemberValid"
class="mt-1 flex items-center gap-2"
>
@@ -553,11 +569,20 @@ defineExpose({
:max-size-mb="1"
v-model="sepFile"
v-bind="sepFileAttrs"
@file-selected="(file: any) => { console.log(file) }"
@file-selected="() => {}"
/>
<span class="mt-1 text-sm text-gray-500">
{{ noteFile }}
</span>
<p v-if="sepFileReview">
<a
class="mt-1 text-sm text-blue-500 capitalize"
href="#"
@click="openFile(sepFileReview.filePath)"
>
{{ sepFileReview?.fileName }}
</a>
</p>
</DE.Cell>
<DE.Cell>
@@ -569,11 +594,20 @@ defineExpose({
:max-size-mb="1"
v-model="sippFile"
v-bind="sippFileAttrs"
@file-selected="(file: any) => { console.log(file) }"
@file-selected="() => {}"
/>
<span class="mt-1 text-sm text-gray-500">
{{ noteFile }}
</span>
<p v-if="sippFileReview">
<a
class="mt-1 text-sm text-blue-500 capitalize"
href="#"
@click="openFile(sippFileReview.filePath)"
>
{{ sippFileReview?.fileName }}
</a>
</p>
</DE.Cell>
</DE.Block>
</template>
@@ -591,13 +625,13 @@ defineExpose({
>
<DE.Cell>
<Label height="compact">Dengan Rujukan / Surat Kontrol</Label>
<Field>
<DE.Field>
<Input
id="sepReference"
v-model="sepReference"
:disabled="true"
/>
</Field>
</DE.Field>
</DE.Cell>
<DE.Cell>
@@ -635,13 +669,13 @@ defineExpose({
>
<DE.Cell :col-span="2">
<Label height="compact">Diagnosis</Label>
<Field>
<DE.Field>
<Input
id="diagnosis"
v-model="diagnosis"
:disabled="true"
/>
</Field>
</DE.Field>
</DE.Cell>
<DE.Cell>
+44 -36
View File
@@ -18,25 +18,13 @@ const props = defineProps<{
onExportCsv?: () => void
}>()
// function emitSearchNavClick() {
// props.refSearchNav?.onClick()
// }
//
// function onInput(event: Event) {
// props.refSearchNav?.onInput((event.target as HTMLInputElement).value)
// }
//
// function btnClick() {
// props.prep?.addNav?.onClick?.()
// }
const emit = defineEmits<{
apply: [filters: { personName: string; startDate: string; endDate: string }]
}>()
const searchQuery = ref('')
const isRoleRegistration = props.activePositon === 'registration'
const isRoleMedical = props.activePositon === 'medical'
const dateRange = ref<{ from: Date | null; to: Date | null }>({
from: new Date(),
to: new Date(),
})
const df = new DateFormatter('en-US', {
dateStyle: 'medium',
@@ -49,31 +37,47 @@ const todayCalendar = new CalendarDate(today.getFullYear(), today.getMonth() + 1
// Get date 1 month ago
const oneMonthAgo = new Date(today)
oneMonthAgo.setMonth(today.getMonth() - 1)
const oneMonthAgoCalendar = new CalendarDate(oneMonthAgo.getFullYear(), oneMonthAgo.getMonth() + 1, oneMonthAgo.getDate())
const oneMonthAgoCalendar = new CalendarDate(
oneMonthAgo.getFullYear(),
oneMonthAgo.getMonth() + 1,
oneMonthAgo.getDate(),
)
const value = ref({
start: oneMonthAgoCalendar,
end: todayCalendar,
}) as Ref<DateRange>
// function onFilterClick() {
// console.log('Search:', searchQuery.value)
// console.log('Date Range:', dateRange.value)
// props.refSearchNav?.onClick()
// }
function onFilterClick() {
const startDate = value.value.start ? value.value.start.toString() : ''
const endDate = value.value.end ? value.value.end.toString() : startDate
emit('apply', {
personName: searchQuery.value,
startDate,
endDate,
})
}
</script>
<template>
<div class="relative w-64">
<Search class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-gray-400" />
<Input v-model="searchQuery" type="text" placeholder="Cari Nama /No.RM" class="pl-9" />
<Input
v-model="searchQuery"
type="text"
placeholder="Cari Nama /No.RM"
class="pl-9"
/>
</div>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn('min-w-[240px] max-w-[320px] justify-start text-left font-normal', !value && 'text-muted-foreground')"
:class="
cn('min-w-[240px] max-w-[320px] justify-start text-left font-normal', !value && 'text-muted-foreground')
"
>
<CalendarIcon class="mr-2 h-4 w-4" />
<template v-if="value.start">
@@ -86,7 +90,7 @@ const value = ref({
{{ df.format(value.start.toDate(getLocalTimeZone())) }}
</template>
</template>
<template v-else> Pick a date </template>
<template v-else>Pick a date</template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
@@ -99,28 +103,32 @@ const value = ref({
</PopoverContent>
</Popover>
<Button variant="outline" class="border-orange-500 text-orange-600 hover:bg-orange-50" @click="onFilterClick">
<Button
variant="outline"
class="border-orange-500 text-orange-600 hover:bg-orange-50"
@click="onFilterClick"
>
<FilterIcon class="mr-2 size-4" />
Filter
</Button>
<DropdownMenu v-show="props.enableExport && (isRoleRegistration || isRoleMedical)">
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto border-orange-500 text-orange-600 hover:bg-orange-50">
<Icon name="i-lucide-download" class="h-4 w-4" />
<Button
variant="outline"
class="ml-auto border-orange-500 text-orange-600 hover:bg-orange-50"
>
<Icon
name="i-lucide-download"
class="h-4 w-4"
/>
Ekspor
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem @click="onExportPdf">
Ekspor PDF
</DropdownMenuItem>
<DropdownMenuItem @click="onExportCsv">
Ekspor CSV
</DropdownMenuItem>
<DropdownMenuItem @click="onExportExcel">
Ekspor Excel
</DropdownMenuItem>
<DropdownMenuItem @click="onExportPdf">Ekspor PDF</DropdownMenuItem>
<DropdownMenuItem @click="onExportCsv">Ekspor CSV</DropdownMenuItem>
<DropdownMenuItem @click="onExportExcel">Ekspor Excel</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
-1
View File
@@ -142,7 +142,6 @@ const onSaveNumber = () => {
// Submit handler
const onSubmit = handleSubmit((values) => {
console.log('✅ Validated form values:', JSON.stringify(values, null, 2))
emit('event', 'save-sep', values)
})
+44 -24
View File
@@ -13,7 +13,11 @@ import { useSidebar } from '~/components/pub/ui/sidebar/utils'
import { getServicePosition } from '~/lib/roles' // previously getPositionAs
// Services
import { getList as getEncounterList, remove as removeEncounter, cancel as cancelEncounter } from '~/services/encounter.service'
import {
getList as getEncounterList,
remove as removeEncounter,
cancel as cancelEncounter,
} from '~/services/encounter.service'
// Apps
import Content from '~/components/app/encounter/list.vue'
@@ -40,6 +44,7 @@ const { getActiveRole } = useUserStore()
// Main data
const data = ref([])
const dataFiltered = ref([])
const filterParams = ref<any>({})
const activeServicePosition = ref(getServicePosition(getActiveRole()))
const isLoading = reactive<DataTableLoader>({
summary: false,
@@ -99,27 +104,30 @@ provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
watch(getActiveRole, (role? : string) => {
watch(getActiveRole, (role?: string) => {
activeServicePosition.value = getServicePosition(role)
})
watch(() => recAction.value, () => {
const basePath = `/${props.classCode}/encounter`
if (recAction.value === ActionEvents.showConfirmDelete) {
isRecordConfirmationOpen.value = true
} else if (recAction.value === ActionEvents.showCancel) {
isRecordCancelOpen.value = true
} else if (recAction.value === ActionEvents.showDetail) {
navigateTo(`${basePath}/${recId.value}/detail`)
} else if (recAction.value === ActionEvents.showEdit) {
navigateTo(`${basePath}/${recId.value}/edit`)
} else if (recAction.value === ActionEvents.showProcess) {
navigateTo(`${basePath}/${recId.value}/process`)
} else if (recAction.value === ActionEvents.showConfirmDelete) {
isRecordConfirmationOpen.value = true
}
recAction.value = '' // reset
})
watch(
() => recAction.value,
() => {
const basePath = `/${props.classCode}/encounter`
if (recAction.value === ActionEvents.showConfirmDelete) {
isRecordConfirmationOpen.value = true
} else if (recAction.value === ActionEvents.showCancel) {
isRecordCancelOpen.value = true
} else if (recAction.value === ActionEvents.showDetail) {
navigateTo(`${basePath}/${recId.value}/detail`)
} else if (recAction.value === ActionEvents.showEdit) {
navigateTo(`${basePath}/${recId.value}/edit`)
} else if (recAction.value === ActionEvents.showProcess) {
navigateTo(`${basePath}/${recId.value}/process`)
} else if (recAction.value === ActionEvents.showConfirmDelete) {
isRecordConfirmationOpen.value = true
}
recAction.value = '' // reset
},
)
onMounted(() => {
getPatientList()
@@ -128,13 +136,16 @@ 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'
data.value = []
try {
const params: any = { includes: 'patient,patient-person' }
const params: any = { includes: includesParams, ...filterParams.value }
if (props.classCode) {
params['class-code'] = props.classCode
params.class_code = props.classCode
}
if (props.subClassCode) {
params['sub-class-code'] = props.subClassCode
params.sub_class_code = props.subClassCode
}
const result = await getEncounterList(params)
if (result.success) {
@@ -148,6 +159,15 @@ async function getPatientList() {
}
}
function handleFilterApply(filters: { personName: string; startDate: string; endDate: string }) {
filterParams.value = {
'person-name': filters.personName,
'start-date': filters.startDate,
'end-date': filters.endDate,
}
getPatientList()
}
// Handle confirmation result
async function handleConfirmCancel(record: any, action: string) {
if (action === 'deactivate' && record?.id) {
@@ -243,7 +263,7 @@ function handleRemoveConfirmation() {
<CH.ContentHeader v-bind="hreaderPrep">
<FilterNav
:active-positon="activeServicePosition"
@onFilterClick="() => isFilterFormDialogOpen = true"
@apply="handleFilterApply"
@onExportPdf="() => {}"
@onExportExcel="() => {}"
@nExportCsv="() => {}"
@@ -259,7 +279,7 @@ function handleRemoveConfirmation() {
size="lg"
prevent-outside
>
<FilterForm v-bind="filter" />
<FilterForm v-bind="filter" />
</Dialog>
<!-- Batal -->
+41 -13
View File
@@ -13,6 +13,9 @@ import { genDoctor, type Doctor } from '~/models/doctor'
// Stores
import { useUserStore } from '~/stores/user'
// Handlers
import { uploadAttachmentCustom } from '~/handlers/supporting-document.handler'
// Services
import {
getList as getSpecialistList,
@@ -26,7 +29,6 @@ import {
} from '~/services/encounter.service'
import { getList as getMemberList } from '~/services/vclaim-member.service'
import { getList as getSepList } from '~/services/vclaim-sep.service'
import { uploadAttachment } from '~/services/supporting-document.service'
// Handlers
import {
@@ -468,6 +470,19 @@ export function useEncounterEntry(props: {
if (formData.subSpecialistId) {
await handleFetchDoctors(formData.subSpecialistId)
}
if (encounter.encounterDocuments && Array.isArray(encounter.encounterDocuments)) {
let sepFileReview = {}
let sippFileReview = {}
for (const doc of encounter.encounterDocuments) {
if (doc.type_code === 'vclaim-sep') {
sepFileReview = { id: doc.id, fileName: doc.fileName, filePath: doc.filePath, type: doc.type_code }
} else if (doc.type_code === 'vclaim-sipp') {
sippFileReview = { id: doc.id, fileName: doc.fileName, filePath: doc.filePath, type: doc.type_code }
}
}
formData.sepFileReview = sepFileReview
formData.sippFileReview = sippFileReview
}
formObjects.value = { ...formData }
}
@@ -501,6 +516,8 @@ export function useEncounterEntry(props: {
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
sepFile.value = formValues.sepFile || null
sippFile.value = formValues.sippFile || null
let paymentMethodCode = formValues.paymentMethod_code ?? null
if (!paymentMethodCode) {
@@ -517,7 +534,7 @@ export function useEncounterEntry(props: {
const payload: any = {
patient_id: patientId,
appointment_doctor_code: formValues.doctorId || null,
appointment_doctor_code: formValues.doctor_code || null,
class_code: props.classCode || '',
subClass_code: props.subClassCode || '',
infra_id: formValues.infra_id ?? null,
@@ -545,18 +562,21 @@ export function useEncounterEntry(props: {
if (paymentMethodCode) {
payload.paymentMethod_code = paymentMethodCode
}
if (!payload.vclaimReference) {
payload.vclaimReference = {
noSep: refNumber,
}
}
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 (isAdmin && props.classCode === 'ambulatory') {
@@ -573,16 +593,26 @@ export function useEncounterEntry(props: {
}
if (result.success) {
console.log(result)
console.log(sepFile.value)
console.log(sippFile.value)
// const encounterId = isEditMode.value ? props.id : result.body?.data?.id
if (patientId) {
const encounterId = isEditMode.value ? props.id : result.body?.data?.id
if (encounterId) {
if (sepFile.value) {
await uploadAttachment(sepFile.value, patientId, 'vclaim-sep')
await uploadAttachmentCustom({
id: isEditMode.value && formValues.sepFileReview ? formValues.sepFileReview.id : null,
file: sepFile.value,
refId: encounterId,
entityTypeCode: 'encounter',
type: 'vclaim-sep',
})
}
if (sippFile.value) {
await uploadAttachment(sippFile.value, patientId, 'vclaim-sipp')
await uploadAttachmentCustom({
id: isEditMode.value && formValues.sippFileReview ? formValues.sippFileReview.id : null,
file: sippFile.value,
refId: encounterId,
entityTypeCode: 'encounter',
type: 'vclaim-sipp',
})
}
}
@@ -620,8 +650,6 @@ export function useEncounterEntry(props: {
paymentsList,
sepsList,
sepNumber,
sepFile,
sippFile,
participantGroupsList,
specialistsTree,
doctorsList,
@@ -256,6 +256,8 @@ export function useIntegrationSepEntry() {
},
]
} else {
const destA = lettersRaw?.rujukan?.provPerujuk?.Kode
const destB = lettersRaw?.rujukan?.provPerujuk?.kode
letters.value = [
{
letterNumber: lettersRaw?.rujukan?.noKunjungan || '',
@@ -276,7 +278,7 @@ export function useIntegrationSepEntry() {
patientName: lettersRaw?.rujukan?.peserta?.nama || '',
patientPhone: lettersRaw?.rujukan?.peserta?.mr?.noTelepon || '',
medicalRecordNumber: lettersRaw?.rujukan?.peserta?.mr?.noMR || '',
destination: lettersRaw?.rujukan?.provPerujuk?.kode || '',
destination: destA || destB || '',
},
},
]
@@ -480,7 +482,7 @@ export function useIntegrationSepEntry() {
}
if (!value.destinationClinic) {
mappedValues.destinationClinic = selectedObjects.value['destination'] || ''
mappedValues.referralTo = selectedObjects.value['destination'] || ''
}
if (!value.clinicExcecutive) {
mappedValues.clinicExcecutive = 'no'
@@ -22,3 +22,17 @@ export const {
update,
remove,
})
export async function uploadAttachmentCustom(payload: any) {
const { user } = useUserStore()
const formData = new FormData()
formData.append('content', payload.file)
formData.append('entityType_code', payload.entityTypeCode)
formData.append('type_code', payload.type)
formData.append('ref_id', payload.refId)
formData.append('upload_employee_id', user.employee_id)
const response = payload.id ? await update(payload.id, formData) : await create(formData)
return response?.body?.data
}