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

Feat: Encounter Adjustment
This commit is contained in:
Munawwirul Jamal
2025-11-26 18:14:23 +07:00
committed by GitHub
122 changed files with 4635 additions and 2631 deletions
@@ -0,0 +1,154 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from '~/components/pub/my-ui/data/types'
import { ActionEvents } from '~/components/pub/my-ui/data/types'
const props = defineProps<{
rec: ListItemDto
}>()
const recId = inject<Ref<number>>('rec_id')!
const recAction = inject<Ref<string>>('rec_action')!
const recItem = inject<Ref<any>>('rec_item')!
const activeKey = ref<string | null>(null)
const activePosition = inject<Ref<string>>('position')!
const linkItemsFiltered = ref<LinkItem[]>([])
const linkItemsBase: LinkItem[] = [
{
label: 'Nothing',
value: 'nothing',
icon: 'i-lucide-file',
},
]
const linkItems: LinkItem[] = [
{
label: 'Detail',
value: 'detail',
onClick: () => {
detail()
},
icon: 'i-lucide-eye',
},
{
label: 'Process',
value: 'edit',
onClick: () => {
edit()
},
icon: 'i-lucide-pencil',
},
{
label: 'Print',
value: 'print',
onClick: () => {
print()
},
icon: 'i-lucide-printer',
},
{
label: 'Batalkan',
value: 'cancel',
onClick: () => {
cancel()
},
icon: 'i-lucide-circle-x',
},
{
label: 'Hapus',
value: 'remove',
onClick: () => {
remove()
},
icon: 'i-lucide-trash',
},
]
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function print() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showPrint
recItem.value = props.rec
}
function cancel() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showCancel
recItem.value = props.rec
}
function remove() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
linkItemsFiltered.value = [...linkItemsBase]
function getLinks(position: string) {
switch (position) {
case 'medical':
linkItemsFiltered.value = [...linkItems]
break
case 'verificator':
linkItemsFiltered.value = [
...linkItems.filter((item) => ['detail', 'print'].includes(item.value || '')),
]
break
default:
linkItemsFiltered.value = [...linkItemsBase]
break
}
}
getLinks(activePosition.value)
watch(activePosition, () => {
getLinks(activePosition.value)
})
</script>
<template>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
>
<Icon
name="i-lucide-chevrons-up-down"
class="ml-auto size-4"
/>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
align="end"
>
<DropdownMenuGroup>
<DropdownMenuItem
v-for="item in linkItemsFiltered"
:key="item.label"
class="hover:bg-gray-100 dark:hover:bg-slate-700"
@click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
>
<Icon :name="item.icon ?? ''" />
<span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>
+9 -3
View File
@@ -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"
/>
<span v-else>+</span>
<Icon
v-else
name="i-lucide-plus"
class="h-4 w-4"
/>
</Button>
<Button
v-else
@@ -0,0 +1,133 @@
<script setup lang="ts">
// 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 } })
}
</script>
<template>
<div class="my-4">
<h2 class="mb-4 text-base font-semibold md:text-lg 2xl:text-xl">History Pasien:</h2>
<div class="flex flex-wrap gap-2 mb-2">
<Button
v-for="item in itemsOne"
:key="item.value"
:class="[
'flex items-center gap-2 rounded-lg border px-3 py-1 text-sm font-medium transition-colors',
activeMenu === item.value
? 'border-orange-300 bg-orange-400 text-white'
: 'border-orange-300 bg-white text-orange-500 hover:bg-orange-400 hover:text-white'
]"
@click="handleClick(item.value)"
>
<Icon v-if="item.type === 'icon'"
:name="item.icon"
class="h-4 w-4"
/>
<img v-else-if="item.type === 'image'" :src="item.icon" class="h-[24px] w-[24px] object-contain" />
{{ item.label }}
</Button>
</div>
<h2 class="mb-4 text-base font-semibold md:text-lg 2xl:text-xl">Billing Pasien:</h2>
<div class="flex flex-wrap gap-2 mb-2">
<Button
v-for="item in itemsTwo"
:key="item.value"
:class="[
'flex items-center gap-2 rounded-lg border px-3 py-1 text-sm font-medium transition-colors',
activeMenu === item.value
? 'border-orange-300 bg-orange-400 text-white'
: 'border-orange-300 bg-white text-orange-500 hover:bg-orange-400 hover:text-white'
]"
@click="handleClick(item.value)"
>
<Icon
:name="item.icon"
class="h-4 w-4"
/>
{{ item.label }}
</Button>
</div>
</div>
</template>
+1 -1
View File
@@ -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 = {
+2 -2
View File
@@ -2,13 +2,13 @@
import { config } from './list.cfg'
const props = defineProps<{
data: any[]
data: any[],
}>()
</script>
<template>
<PubMyUiDataTable
v-bind="config"
:rows="data"
:rows="props.data"
/>
</template>
@@ -0,0 +1,28 @@
<script setup lang="ts">
// Types
import type { Encounter } from '~/models/encounter'
// Components
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '~/components/pub/ui/accordion'
import EncounterQuickInfoFull from '~/components/app/encounter/quick-info-full.vue'
const props = defineProps<{
data: Encounter
}>()
</script>
<template>
<div class="w-full rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
<!-- Data Pasien -->
<Accordion type="single" defaultValue="item-patient" collapsible>
<AccordionItem value="item-patient" class="border-none">
<AccordionTrigger class="focus:outline-none focus:ring-0">
<h2 class="mb-4 text-base font-semibold md:text-lg 2xl:text-xl">Data Pasien:</h2>
</AccordionTrigger>
<AccordionContent>
<EncounterQuickInfoFull :data="props.data" :is-grid="true" />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</template>
@@ -0,0 +1,18 @@
<script setup lang="ts">
// Types
import type { Encounter } from '~/models/encounter'
// Components
import EncounterQuickInfoFull from '~/components/app/encounter/quick-info-full.vue'
const props = defineProps<{
data: Encounter
}>()
</script>
<template>
<div class="w-full rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
<h2 class="mb-4 text-base font-semibold md:text-lg 2xl:text-xl">Data Pasien:</h2>
<EncounterQuickInfoFull :data="props.data" :is-grid="false" />
</div>
</template>
@@ -0,0 +1,247 @@
<script setup lang="ts">
// Helpers
import { format, parseISO } from 'date-fns'
import { id as localeID } from 'date-fns/locale'
import { getAge } from '~/lib/date'
import { cn } from "~/lib/utils"
// Types
import type { Encounter } from '~/models/encounter'
import { paymentTypes } from '~/lib/constants.vclaim'
const props = defineProps<{
data: Encounter
isGrid?: boolean
}>()
const isGrid = props.isGrid !== undefined ? props.isGrid : true
// Address
const address = computed(() => {
if (props.data.patient.person.addresses && props.data.patient.person.addresses.length > 0) {
return props.data.patient.person.addresses.map((a) => a.address).join(', ')
}
return '-'
})
// DPJP
const dpjp = computed(() => {
if (props.data.responsible_doctor) {
const dp = props.data.responsible_doctor.employee.person
return `${dp.frontTitle || ''} ${dp.name || ''} ${dp.endTitle || ''}`.trim()
} else if (props.data.appointment_doctor) {
const dp = props.data.appointment_doctor.employee.person
return `${dp.frontTitle || ''} ${dp.name || ''} ${dp.endTitle || ''}`.trim()
}
return '-'
})
// Tgl. Lahir dengan Umur
const birthDateFormatted = computed(() => {
if (!props.data.patient.person.birthDate) {
return '-'
}
try {
const ageData = getAge(props.data.patient.person.birthDate)
const ageYears = ageData.extFormat.split(' ')[0] || '0'
return `${ageData.idFormat} (${ageYears} Tahun)`
} catch {
return '-'
}
})
// Tgl. Masuk RS dengan waktu
const registeredDateFormatted = computed(() => {
const dateStr = props.data.registeredAt || props.data.visitDate
if (!dateStr) {
return '-'
}
try {
const date = parseISO(dateStr)
return format(date, 'dd MMMM yyyy HH:mm', { locale: localeID })
} catch {
return '-'
}
})
// Jns. Kelamin
const genderLabel = computed(() => {
const code = props.data.patient.person.gender_code
if (!code) return '-'
// Map common gender codes
if (code === 'M' || code === 'male' || code.toLowerCase() === 'l') {
return 'Laki-laki'
} else if (code === 'F' || code === 'female' || code.toLowerCase() === 'p') {
return 'Perempuan'
}
return code
})
// Jns. Pembayaran
const paymentTypeLabel = computed(() => {
const code = props.data.paymentMethod_code
if (!code) return '-'
// Map payment method codes
if (code === 'insurance') {
return 'JKN'
} else if (code === 'jkn') {
return 'JKN'
} else if (code === 'jkmm') {
return 'JKMM'
} else if (code === 'spm') {
return 'SPM'
} else if (code === 'pks') {
return 'PKS'
}
// Try to get from paymentTypes constant
if (paymentTypes[code]) {
return paymentTypes[code].split(' ')[0] // Get first part (e.g., "JKN" from "JKN (Jaminan...)")
}
return code
})
// No. Billing - try to get from trx_number or other billing fields
const billingNumber = computed(() => {
// Check if encounter has payment data with trx_number
if ((props.data as any).trx_number) {
return (props.data as any).trx_number
}
// Check if encounter has payment relation
if ((props.data as any).payment?.trx_number) {
return (props.data as any).payment.trx_number
}
return '-'
})
// Nama Ruang - from unit name or room relation
const roomName = computed(() => {
if (props.data.unit?.name) {
return props.data.unit.name
}
// Check if there's a room relation
if ((props.data as any).room?.name) {
return (props.data as any).room.name
}
return '-'
})
// No Bed - from bed relation or field
const bedNumber = computed(() => {
// Check if encounter has bed data
if ((props.data as any).bed?.number) {
return (props.data as any).bed.number
}
if ((props.data as any).bed_number) {
return (props.data as any).bed_number
}
return '-'
})
</script>
<template>
<h2 class="mb-4 font-semibold md:text-base 2xl:text-lg">Data Pasien:</h2>
<!-- 4 Column Grid Layout -->
<div :class="cn('grid grid-cols-4 gap-4', isGrid && 'sm:grid-cols-4')">
<!-- No. RM -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">No. RM</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ data.patient.number || '-' }}
</p>
</div>
<!-- Tgl. Lahir -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Tgl. Lahir</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ birthDateFormatted }}
</p>
</div>
<!-- Jns. Pembayaran -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Cara Bayar</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ paymentTypeLabel }}
</p>
</div>
<!-- No Bed -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">No Bed</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ bedNumber }}
</p>
</div>
<!-- Nama Pasien -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">No. Bed</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ data.patient.person.name || '-' }}
</p>
</div>
<!-- Tgl. Masuk RS -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Tgl. Masuk RS</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ registeredDateFormatted }}
</p>
</div>
<!-- No. Billing -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">No. Billing</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ billingNumber }}
</p>
</div>
<!-- DPJP -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">DPJP</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ dpjp }}
</p>
</div>
<!-- Alamat -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Alamat</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ address }}
</p>
</div>
<!-- Jns. Kelamin -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Jns. Kelamin</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ genderLabel }}
</p>
</div>
<!-- Nama Ruang -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Nama Ruang</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ roomName }}
</p>
</div>
</div>
</template>
@@ -0,0 +1,130 @@
<script setup lang="ts">
// 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 } })
}
</script>
<template>
<h2 class="mb-4 font-semibold md:text-base 2xl:text-lg">Menu Cepat:</h2>
<div class="flex flex-wrap gap-2 mb-2">
<Button
v-for="item in itemsOne"
:key="item.value"
:class="[
'flex items-center gap-2 rounded-lg border px-3 py-1 text-sm font-medium transition-colors',
activeMenu === item.value
? 'border-orange-300 bg-orange-400 text-white'
: 'border-orange-300 bg-white text-orange-500 hover:bg-orange-400 hover:text-white'
]"
@click="handleClick(item.value)"
>
<Icon v-if="item.type === 'icon'"
:name="item.icon"
class="h-4 w-4"
/>
<img v-else-if="item.type === 'image'" :src="item.icon" class="h-[24px] w-[24px] object-contain" />
{{ item.label }}
</Button>
</div>
<div class="flex flex-wrap gap-2 mb-2">
<Button
v-for="item in itemsTwo"
:key="item.value"
:class="[
'flex items-center gap-2 rounded-lg border px-3 py-1 text-sm font-medium transition-colors',
activeMenu === item.value
? 'border-orange-300 bg-orange-400 text-white'
: 'border-orange-300 bg-white text-orange-500 hover:bg-orange-400 hover:text-white'
]"
@click="handleClick(item.value)"
>
<Icon
:name="item.icon"
class="h-4 w-4"
/>
{{ item.label }}
</Button>
</div>
</template>
@@ -12,7 +12,7 @@ const statusCodeColors: Record<string, Variants> = {
review: 'fresh',
process: 'fresh',
done: 'positive',
canceled: 'destructive',
cancel: 'destructive',
rejected: 'destructive',
skiped: 'negative',
}
+7
View File
@@ -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'
}
})
</script>
+2 -1
View File
@@ -11,7 +11,8 @@ const props = defineProps<{
<template>
<PubMyUiDataTable
v-bind="config"
:rows="props.data"
v-bind="config"
v-on="$attrs"
/>
</template>
@@ -3,5 +3,5 @@ import EncounterHome from '~/components/content/encounter/home.vue'
</script>
<template>
<EncounterHome classes="chemotherapy" />
<EncounterHome display="menu" class-code="ambulatory" sub-class-code="chemo" />
</template>
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
// Components
import EncounterPatientInfo from '~/components/app/encounter/patient-info.vue'
// Models
import { genEncounter } from '~/models/encounter'
// Handlers
import { getEncounterData } from '~/handlers/encounter-process.handler'
const route = useRoute()
const router = useRouter()
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const data = ref<any>(genEncounter())
const isShowPatient = computed(() => data.value && data.value?.patient?.person)
async function getData() {
data.value = await getEncounterData(id)
}
onMounted(async () => {
await getData()
})
function handleClick(type: string) {
if (type === 'draft') {
router.back()
}
}
</script>
<template>
<div class="w-full">
<div class="mb-4">
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" @click="handleClick" />
</div>
<EncounterPatientInfo v-if="isShowPatient" :data="data" />
</div>
</template>
+46 -621
View File
@@ -1,42 +1,14 @@
<script setup lang="ts">
// Components
import { toast } from '~/components/pub/ui/toast'
import { Button } from '~/components/pub/ui/button'
import AppEncounterEntryForm from '~/components/app/encounter/entry-form.vue'
import AppViewPatient from '~/components/app/patient/view-patient.vue'
// Types
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
// Constants
import { paymentTypes, sepRefTypeCodes, participantGroups } from '~/lib/constants.vclaim'
// Services
import {
getList as getSpecialistList,
getValueTreeItems as getSpecialistTreeItems,
} from '~/services/specialist.service'
import { getValueLabelList as getDoctorValueLabelList } from '~/services/doctor.service'
import { create as createEncounter, getDetail as getEncounterDetail, update as updateEncounter } from '~/services/encounter.service'
import { getList as getSepList } from '~/services/vclaim-sep.service'
// Helpers
import { refDebounced } from '@vueuse/core'
// Handlers
import {
patients,
selectedPatient,
selectedPatientObject,
paginationMeta,
getPatientsList,
getPatientCurrent,
getPatientByIdentifierSearch,
} from '~/handlers/patient.handler'
// Stores
import { useUserStore } from '~/stores/user'
import { useEncounterEntry } from '~/handlers/encounter-entry.handler'
const props = defineProps<{
id: number
@@ -46,54 +18,43 @@ const props = defineProps<{
}>()
const route = useRoute()
const userStore = useUserStore()
const openPatient = ref(false)
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
})
const paymentsList = ref<Array<{ value: string; label: string }>>([])
const sepsList = ref<Array<{ value: string; label: string }>>([])
const participantGroupsList = ref<Array<{ value: string; label: string }>>([])
const specialistsTree = ref<TreeItem[]>([])
const specialistsData = ref<any[]>([]) // Store full specialist data with id
const doctorsList = ref<Array<{ value: string; label: string }>>([])
const recSelectId = ref<number | null>(null)
const isSaving = ref(false)
const isLoadingDetail = ref(false)
const formRef = ref<InstanceType<typeof AppEncounterEntryForm> | null>(null)
const encounterData = ref<any>(null)
const formObjects = ref<any>({})
// SEP validation state
const isSepValid = ref(false)
const isCheckingSep = ref(false)
const sepNumber = ref('')
const {
paymentsList,
sepNumber,
sepsList,
participantGroupsList,
specialistsTree,
doctorsList,
recSelectId,
isLoadingDetail,
formObjects,
openPatient,
isSepValid,
isCheckingSep,
isSaveDisabled,
isSaving,
isLoading,
patients,
selectedPatient,
selectedPatientObject,
paginationMeta,
toNavigateSep,
getListPath,
handleInit,
loadEncounterDetail,
handleSaveEncounter,
getPatientsList,
getPatientCurrent,
getPatientByIdentifierSearch,
getIsSubspecialist,
getValidateSepNumber,
handleFetchDoctors,
} = useEncounterEntry(props)
const debouncedSepNumber = refDebounced(sepNumber, 500)
// Computed for edit mode
const isEditMode = computed(() => props.id > 0)
// Computed for save button disable state
const isSaveDisabled = computed(() => {
return !selectedPatient.value || !selectedPatientObject.value || isSaving.value || isLoadingDetail.value
})
function getListPath(): string {
if (props.classCode === 'ambulatory' && props.subClassCode === 'rehab') {
return '/rehab/encounter'
}
if (props.classCode === 'ambulatory' && props.subClassCode === 'reg') {
return '/outpatient/encounter'
}
if (props.classCode === 'emergency') {
return '/emergency/encounter'
}
if (props.classCode === 'inpatient') {
return '/inpatient/encounter'
}
return '/encounter' // fallback
}
function handleSavePatient() {
selectedPatientObject.value = null
setTimeout(() => {
@@ -101,140 +62,15 @@ function handleSavePatient() {
}, 150)
}
function toKebabCase(str: string): string {
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
function handleSaveClick() {
if (formRef.value && typeof formRef.value.submitForm === 'function') {
formRef.value.submitForm()
}
}
function toNavigateSep(values: any) {
const queryParams = new URLSearchParams()
if (values['subSpecialistCode']) {
const isSub = isSubspecialist(values['subSpecialistCode'], specialistsTree.value)
if (!isSub) {
values['specialistCode'] = values['subSpecialistCode']
delete values['subSpecialistCode']
}
}
Object.keys(values).forEach((field) => {
if (values[field]) {
queryParams.append(toKebabCase(field), values[field])
}
})
navigateTo('/integration/bpjs/sep/add' + `?${queryParams.toString()}`)
}
async function handleSaveEncounter(formValues: any) {
// Validate patient is selected
if (!selectedPatient.value || !selectedPatientObject.value) {
toast({
title: 'Gagal',
description: 'Pasien harus dipilih terlebih dahulu',
variant: 'destructive',
})
return
}
try {
isSaving.value = true
// Get employee_id from user store
const employeeId = userStore.user?.employee_id || userStore.user?.employee?.id || 0
// Format date to ISO format
const formatDate = (dateString: string): string => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toISOString()
}
// Get specialist_id and subspecialist_id from TreeSelect value (code)
const { specialist_id, subspecialist_id } = getSpecialistIdsFromCode(formValues.subSpecialistId || '')
// Build payload
const payload: any = {
patient_id: selectedPatientObject.value?.id || Number(selectedPatient.value),
registeredAt: formatDate(formValues.registerDate),
visitDate: formatDate(formValues.registerDate),
class_code: props.classCode || '',
subClass_code: props.subClassCode || '',
infra_id: null,
unit_id: null,
appointment_doctor_id: Number(formValues.doctorId),
responsible_doctor_id: Number(formValues.doctorId),
paymentType: formValues.paymentType,
cardNumber: formValues.cardNumber,
refSource_name: '',
appointment_id: null,
}
if (employeeId && employeeId > 0) {
payload.adm_employee_id = employeeId
}
// Add specialist_id and subspecialist_id if available
if (specialist_id) {
payload.specialist_id = specialist_id
}
if (subspecialist_id) {
payload.subspecialist_id = subspecialist_id
}
let paymentMethod = 'cash'
if (formValues.paymentType === 'jkn' || formValues.paymentType === 'jkmm') {
paymentMethod = 'insurance'
} else if (formValues.paymentType === 'spm') {
paymentMethod = 'cash'
} else if (formValues.paymentType === 'pks') {
paymentMethod = 'membership'
}
if (paymentMethod === 'insurance') {
payload.paymentMethod_code = paymentMethod
payload.insuranceCompany_id = null
payload.member_number = formValues.cardNumber
payload.ref_number = formValues.sepNumber
}
// Add visitMode_code and allocatedVisitCount only if classCode is ambulatory
if (props.classCode === 'ambulatory') {
payload.visitMode_code = 'adm'
payload.allocatedVisitCount = 0
}
// Call encounter service - use update if edit mode, create otherwise
let result
if (isEditMode.value) {
result = await updateEncounter(props.id, payload)
} else {
result = await createEncounter(payload)
}
if (result.success) {
toast({
title: 'Berhasil',
description: isEditMode.value ? 'Kunjungan berhasil diperbarui' : 'Kunjungan berhasil dibuat',
variant: 'default',
})
// Redirect to list page
await navigateTo(getListPath())
} else {
const errorMessage = result.body?.message || (isEditMode.value ? 'Gagal memperbarui kunjungan' : 'Gagal membuat kunjungan')
toast({
title: 'Gagal',
description: errorMessage,
variant: 'destructive',
})
}
} catch (error: any) {
console.error('Error saving encounter:', error)
toast({
title: 'Gagal',
description: error?.message || (isEditMode.value ? 'Gagal memperbarui kunjungan' : 'Gagal membuat kunjungan'),
variant: 'destructive',
})
} finally {
isSaving.value = false
function handleFetch(value?: any) {
if (value?.subSpecialistId) {
handleFetchDoctors(value.subSpecialistId)
}
}
@@ -246,11 +82,9 @@ async function handleEvent(menu: string, value?: any) {
} else if (menu === 'add') {
navigateTo('/client/patient/add')
} else if (menu === 'add-sep') {
// If SEP is already valid, don't navigate
if (isSepValid.value) {
return
}
recSelectId.value = null
toNavigateSep({
isService: 'false',
sourcePath: route.path,
@@ -258,10 +92,7 @@ async function handleEvent(menu: string, value?: any) {
...value,
})
} else if (menu === 'sep-number-changed') {
// Update sepNumber when it changes in form (only if different to prevent loop)
if (sepNumber.value !== value) {
sepNumber.value = value || ''
}
await getValidateSepNumber(String(value || ''))
} else if (menu === 'save') {
await handleSaveEncounter(value)
} else if (menu === 'cancel') {
@@ -269,432 +100,26 @@ async function handleEvent(menu: string, value?: any) {
}
}
// Handle save button click
function handleSaveClick() {
console.log('🔵 handleSaveClick called')
console.log('🔵 formRef:', formRef.value)
console.log('🔵 isSaveDisabled:', isSaveDisabled.value)
console.log('🔵 selectedPatient:', selectedPatient.value)
console.log('🔵 selectedPatientObject:', selectedPatientObject.value)
console.log('🔵 isSaving:', isSaving.value)
console.log('🔵 isLoadingDetail:', isLoadingDetail.value)
if (formRef.value && typeof formRef.value.submitForm === 'function') {
console.log('🔵 Calling formRef.value.submitForm()')
formRef.value.submitForm()
} else {
console.error('❌ formRef.value is null or submitForm is not a function')
}
}
provide('rec_select_id', recSelectId)
provide('table_data_loader', isLoading)
/**
* Validate SEP number
*/
async function validateSepNumber(sepNumberValue: string) {
// Reset validation if SEP number is empty
if (!sepNumberValue || sepNumberValue.trim() === '') {
isSepValid.value = false
isCheckingSep.value = false
return
}
// Only check if payment type is JKN
// We need to check from formObjects
const paymentType = formObjects.value?.paymentType
if (paymentType !== 'jkn') {
isSepValid.value = false
return
}
try {
isCheckingSep.value = true
const result = await getSepList({ number: sepNumberValue.trim() })
// Check if SEP is found
// If response is not null, SEP is found
// If response is null with metaData code "201", SEP is not found
if (result.success && result.body?.response !== null) {
isSepValid.value = true
} else {
// SEP not found (response null with metaData code "201")
isSepValid.value = false
}
} catch (error) {
console.error('Error checking SEP:', error)
isSepValid.value = false
} finally {
isCheckingSep.value = false
}
}
// Watch debounced SEP number to validate
watch(debouncedSepNumber, async (newValue) => {
await validateSepNumber(newValue)
await getValidateSepNumber(newValue)
})
// Watch payment type to reset SEP validation
watch(
() => formObjects.value?.paymentType,
(newValue) => {
isSepValid.value = false
// If payment type is not JKN, clear SEP number
if (newValue !== 'jkn') {
sepNumber.value = ''
}
},
)
async function handleFetchSpecialists() {
try {
const specialistsResult = await getSpecialistList({ 'page-size': 100, includes: 'subspecialists' })
if (specialistsResult.success) {
const specialists = specialistsResult.body?.data || []
specialistsData.value = specialists // Store full data for mapping
specialistsTree.value = getSpecialistTreeItems(specialists)
}
} catch (error) {
console.error('Error fetching specialist-subspecialist tree:', error)
}
}
/**
* Helper function to check if a value exists in the specialistsTree
* Returns true if it's a leaf node (subspecialist), false if parent node (specialist)
*/
function isSubspecialist(value: string, items: TreeItem[]): boolean {
for (const item of items) {
if (item.value === value) {
// If this item has children, it's not selected, so skip
// If this is the selected item, check if it has children in the tree
return false // This means it's a specialist, not a subspecialist
}
if (item.children) {
for (const child of item.children) {
if (child.value === value) {
// This is a subspecialist (leaf node)
return true
}
}
}
}
return false
}
/**
* Helper function to get specialist/subspecialist code from ID
* Returns code string or null if not found
*/
function getSpecialistCodeFromId(id: number | null | undefined): string | null {
if (!id) return null
// First check if encounter has specialist object with code
if (encounterData.value?.specialist?.id === id) {
return encounterData.value.specialist.code || null
}
// Search in specialistsData
for (const specialist of specialistsData.value) {
if (specialist.id === id) {
return specialist.code || null
}
// Check subspecialists
if (specialist.subspecialists && Array.isArray(specialist.subspecialists)) {
for (const subspecialist of specialist.subspecialists) {
if (subspecialist.id === id) {
return subspecialist.code || null
}
}
}
}
return null
}
/**
* Helper function to get subspecialist code from ID
* Returns code string or null if not found
*/
function getSubspecialistCodeFromId(id: number | null | undefined): string | null {
if (!id) return null
// First check if encounter has subspecialist object with code
if (encounterData.value?.subspecialist?.id === id) {
return encounterData.value.subspecialist.code || null
}
// Search in specialistsData
for (const specialist of specialistsData.value) {
if (specialist.subspecialists && Array.isArray(specialist.subspecialists)) {
for (const subspecialist of specialist.subspecialists) {
if (subspecialist.id === id) {
return subspecialist.code || null
}
}
}
}
return null
}
/**
* Helper function to find specialist_id and subspecialist_id from TreeSelect value (code)
* Returns { specialist_id: number | null, subspecialist_id: number | null }
*/
function getSpecialistIdsFromCode(code: string): { specialist_id: number | null; subspecialist_id: number | null } {
if (!code) {
return { specialist_id: null, subspecialist_id: null }
}
// Check if it's a subspecialist
const isSub = isSubspecialist(code, specialistsTree.value)
if (isSub) {
// Find subspecialist and its parent specialist
for (const specialist of specialistsData.value) {
if (specialist.subspecialists && Array.isArray(specialist.subspecialists)) {
for (const subspecialist of specialist.subspecialists) {
if (subspecialist.code === code) {
return {
specialist_id: specialist.id ? Number(specialist.id) : null,
subspecialist_id: subspecialist.id ? Number(subspecialist.id) : null,
}
}
}
}
}
} else {
// It's a specialist
for (const specialist of specialistsData.value) {
if (specialist.code === code) {
return {
specialist_id: specialist.id ? Number(specialist.id) : null,
subspecialist_id: null,
}
}
}
}
return { specialist_id: null, subspecialist_id: null }
}
async function handleFetchDoctors(subSpecialistId: string | null = null) {
try {
// Build filter based on selection type
const filterParams: any = { 'page-size': 100, includes: 'employee-Person' }
if (!subSpecialistId) {
const doctors = await getDoctorValueLabelList(filterParams)
doctorsList.value = doctors
return
}
// Check if the selectd value is a subspecialist or specialist
const isSub = isSubspecialist(subSpecialistId, specialistsTree.value)
if (isSub) {
// If selected is subspecialist, filter by subspecialist-id
filterParams['subspecialist-id'] = subSpecialistId
} else {
// If selected is specialist, filter by specialist-id
filterParams['specialist-id'] = subSpecialistId
}
const doctors = await getDoctorValueLabelList(filterParams)
doctorsList.value = doctors
} catch (error) {
console.error('Error fetching doctors:', error)
doctorsList.value = []
}
}
function handleFetch(value?: any) {
if (value?.subSpecialistId) {
// handleFetchDoctors(value.subSpecialistId)
}
}
async function handleInit() {
selectedPatientObject.value = null
paymentsList.value = Object.keys(paymentTypes).map((item) => ({
value: item.toString(),
label: paymentTypes[item],
})) as any
sepsList.value = Object.keys(sepRefTypeCodes).map((item) => ({
value: item.toString(),
label: sepRefTypeCodes[item],
})) as any
participantGroupsList.value = Object.keys(participantGroups).map((item) => ({
value: item.toString(),
label: participantGroups[item],
})) as any
// Fetch tree data
await handleFetchDoctors()
await handleFetchSpecialists()
}
/**
* Load encounter detail data for edit mode
*/
async function loadEncounterDetail() {
if (!isEditMode.value || props.id <= 0) {
return
}
try {
isLoadingDetail.value = true
// Include patient, person, specialist, and subspecialist in the response
const result = await getEncounterDetail(props.id, {
includes: 'patient,patient-person,specialist,subspecialist',
})
if (result.success && result.body?.data) {
encounterData.value = result.body.data
await mapEncounterToForm(encounterData.value)
// Set loading to false after mapping is complete
isLoadingDetail.value = false
} else {
toast({
title: 'Gagal',
description: 'Gagal memuat data kunjungan',
variant: 'destructive',
})
// Redirect to list page if encounter not found
await navigateTo(getListPath())
}
} catch (error: any) {
console.error('Error loading encounter detail:', error)
toast({
title: 'Gagal',
description: error?.message || 'Gagal memuat data kunjungan',
variant: 'destructive',
})
// Redirect to list page on error
await navigateTo(getListPath())
} finally {
isLoadingDetail.value = false
}
}
/**
* Map encounter data to form fields
*/
async function mapEncounterToForm(encounter: any) {
if (!encounter) return
// Set patient data - use data from response if available
if (encounter.patient) {
selectedPatient.value = String(encounter.patient.id)
selectedPatientObject.value = encounter.patient
// Only fetch patient if person data is missing
if (!encounter.patient.person) {
await getPatientCurrent(selectedPatient.value)
}
}
// Map form fields
const formData: any = {}
// Patient data (readonly, populated from selected patient)
// Use encounter.patient.person which is already in the response
if (encounter.patient?.person) {
formData.patientName = encounter.patient.person.name || ''
formData.nationalIdentity = encounter.patient.person.residentIdentityNumber || ''
formData.medicalRecordNumber = encounter.patient.number || ''
} else if (selectedPatientObject.value?.person) {
// Fallback to selectedPatientObject if encounter.patient.person is not available
formData.patientName = selectedPatientObject.value.person.name || ''
formData.nationalIdentity = selectedPatientObject.value.person.residentIdentityNumber || ''
formData.medicalRecordNumber = selectedPatientObject.value.number || ''
}
// Doctor ID
const doctorId = encounter.appointment_doctor_id || encounter.responsible_doctor_id
if (doctorId) {
formData.doctorId = String(doctorId)
}
// Specialist/Subspecialist - use helper function to get code from ID
// Priority: subspecialist_id > specialist_id
if (encounter.subspecialist_id) {
const subspecialistCode = getSubspecialistCodeFromId(encounter.subspecialist_id)
if (subspecialistCode) {
formData.subSpecialistId = subspecialistCode
}
} else if (encounter.specialist_id) {
const specialistCode = getSpecialistCodeFromId(encounter.specialist_id)
if (specialistCode) {
formData.subSpecialistId = specialistCode
}
}
// Fallback: if encounter has specialist/subspecialist object with code
if (!formData.subSpecialistId) {
if (encounter.subspecialist?.code) {
formData.subSpecialistId = encounter.subspecialist.code
} else if (encounter.specialist?.code) {
formData.subSpecialistId = encounter.specialist.code
}
}
// Register date
if (encounter.registeredAt) {
// Convert ISO date to local date string (YYYY-MM-DD)
const date = new Date(encounter.registeredAt)
formData.registerDate = date.toISOString().split('T')[0]
} else if (encounter.visitDate) {
const date = new Date(encounter.visitDate)
formData.registerDate = date.toISOString().split('T')[0]
}
// Payment data - use fields directly from encounter
// Map paymentMethod_code to paymentType
if (encounter.paymentMethod_code) {
// Map paymentMethod_code to paymentType
// 'insurance' typically means JKN/JKMM
if (encounter.paymentMethod_code === 'insurance') {
formData.paymentType = 'jkn' // Default to JKN for insurance
} else {
// For other payment methods, use the code directly if it matches
// Otherwise default to 'spm'
const validPaymentTypes = ['jkn', 'jkmm', 'spm', 'pks']
if (validPaymentTypes.includes(encounter.paymentMethod_code)) {
formData.paymentType = encounter.paymentMethod_code
} else {
formData.paymentType = 'spm' // Default to SPM
}
}
} else {
// If paymentMethod_code is empty or null, default to 'spm'
formData.paymentType = 'spm'
}
// Map payment fields directly from encounter
formData.cardNumber = encounter.member_number || ''
formData.sepNumber = encounter.ref_number || ''
// Note: patientCategory and sepType might not be available in the response
// These fields might need to be set manually or fetched from other sources
// Set form objects for the form component
formObjects.value = formData
// Update sepNumber for validation
if (formData.sepNumber) {
sepNumber.value = formData.sepNumber
}
// Fetch doctors based on specialist/subspecialist selection
if (formData.subSpecialistId) {
await handleFetchDoctors(formData.subSpecialistId)
}
}
provide('rec_select_id', recSelectId)
provide('table_data_loader', isLoading)
onMounted(async () => {
await handleInit()
// Load encounter detail if in edit mode
if (isEditMode.value) {
if (props.id > 0) {
await loadEncounterDetail()
}
})
-190
View File
@@ -1,190 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getDetail } from '~/services/encounter.service'
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
// PLASE ORDER BY TAB POSITION
import Status from '~/components/content/encounter/status.vue'
import AssesmentFunctionList from '~/components/content/assesment-function/list.vue'
import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
import PrescriptionList from '~/components/content/prescription/list.vue'
import Consultation from '~/components/content/consultation/list.vue'
import ProtocolList from '~/components/app/chemotherapy/list.protocol.vue'
import MedicineProtocolList from '~/components/app/chemotherapy/list.medicine.vue'
const route = useRoute()
const router = useRouter()
const props = defineProps<{
classes?: string
}>()
// activeTab selalu sinkron dengan query param
const activeTab = computed({
get: () => (route.query?.tab && typeof route.query.tab === 'string' ? route.query.tab : 'status'),
set: (val: string) => {
router.replace({ path: route.path, query: { tab: val } })
},
})
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
// const dataRes = await getDetail(id, {
// includes:
// 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person',
// })
// const dataResBody = dataRes.body ?? null
// const data = dataResBody?.data ?? null
// Dummy data so AppEncounterQuickInfo can render in development/storybook
// Replace with real API result when available (see commented fetch below)
const data = ref<any>({
patient: {
number: 'RM-2025-0001',
person: {
name: 'John Doe',
birthDate: '1980-01-01T00:00:00Z',
gender_code: 'M',
addresses: [{ address: 'Jl. Contoh No.1, Jakarta' }],
frontTitle: '',
endTitle: '',
},
},
visitDate: new Date().toISOString(),
unit: { name: 'Onkologi' },
responsible_doctor: null,
appointment_doctor: { employee: { person: { name: 'Dr. Clara Smith', frontTitle: 'Dr.', endTitle: 'Sp.OG' } } },
})
// Dummy rows for ProtocolList (matches keys expected by list-cfg.protocol)
const protocolRows = [
{
number: '1',
tanggal: new Date().toISOString().substring(0, 10),
siklus: 'I',
periode: 'Siklus I',
kehadiran: 'Hadir',
action: '',
},
{
number: '2',
tanggal: new Date().toISOString().substring(0, 10),
siklus: 'II',
periode: 'Siklus II',
kehadiran: 'Tidak Hadir',
action: '',
},
]
const paginationMeta = {
recordCount: protocolRows.length,
page: 1,
pageSize: 10,
totalPage: 1,
hasNext: false,
hasPrev: false,
}
const tabsRaws: TabItem[] = [
{
value: 'status',
label: 'Status Masuk/Keluar',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: Status,
props: { encounter: data },
},
{
value: 'early-medical-assessment',
label: 'Pengkajian Awal Medis',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: EarlyMedicalAssesmentList,
},
{
value: 'rehab-medical-assessment',
label: 'Pengkajian Awal Medis Rehabilitasi Medis',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: EarlyMedicalRehabList,
},
{
value: 'function-assessment',
label: 'Asesmen Fungsi',
groups: ['ambulatory', 'rehabilitation'],
component: AssesmentFunctionList,
},
{ value: 'therapy-protocol', groups: ['ambulatory', 'rehabilitation'], label: 'Protokol Terapi' },
{
value: 'chemotherapy-protocol',
label: 'Protokol Kemoterapi',
groups: ['chemotherapy'],
component: ProtocolList,
props: { data: protocolRows, paginationMeta },
},
{
value: 'chemotherapy-medicine',
label: 'Protokol Obat Kemoterapi',
groups: ['chemotherapy'],
component: MedicineProtocolList,
props: { data: protocolRows, paginationMeta },
},
{ value: 'report', label: 'Laporan Tindakan', groups: ['chemotherapy'] },
{ value: 'patient-note', label: 'CPRJ', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{
value: 'education-assessment',
label: 'Asesmen Kebutuhan Edukasi',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
},
{ value: 'consent', label: 'General Consent', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'patient-note', label: 'CPRJ', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{
value: 'prescription',
label: 'Order Obat',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: PrescriptionList,
},
{ value: 'device', label: 'Order Alkes', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'mcu-radiology', label: 'Order Radiologi', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'mcu-lab-pc', label: 'Order Lab PK', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'mcu-lab-micro', label: 'Order Lab Mikro', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'mcu-lab-pa', label: 'Order Lab PA', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'medical-action', label: 'Order Ruang Tindakan', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'mcu-result', label: 'Hasil Penunjang', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{
value: 'consultation',
label: 'Konsultasi',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: Consultation,
props: { encounter: data },
},
{ value: 'resume', label: 'Resume', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'control', label: 'Surat Kontrol', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'screening', label: 'Skrinning MPP', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung', groups: ['ambulatory', 'rehabilitation'] },
{ value: 'price-list', label: 'Tarif Tindakan', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
]
const tabs = computed(() => {
return tabsRaws
.filter((tab: TabItem) => tab.groups ? tab.groups.some((group: string) => props.classes?.includes(group)) : true)
.map((tab: TabItem) => {
return { ...tab, props: { ...tab.props, encounter: data } }
})
})
</script>
<template>
<div class="w-full">
<div class="mb-4">
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" />
</div>
<AppEncounterQuickInfo :data="data" />
<CompTab
:data="tabs"
:initial-active-tab="activeTab"
@change-tab="activeTab = $event"
/>
</div>
</template>
+90 -4
View File
@@ -7,6 +7,9 @@ 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'
// Libs
import { getPositionAs } from '~/lib/roles'
// Types
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
@@ -14,11 +17,14 @@ 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 { 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 +41,7 @@ const recAction = ref<string>('')
const recItem = ref<any>(null)
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
const isRecordCancelOpen = ref(false)
const hreaderPrep: HeaderPrep = {
title: 'Kunjungan',
@@ -114,6 +121,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 +196,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,6 +218,11 @@ watch(
isRecordConfirmationOpen.value = true
return
}
if (recAction.value === ActionEvents.showCancel) {
isRecordCancelOpen.value = true
return
}
const basePath = getBasePath()
@@ -173,9 +230,9 @@ watch(
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 +251,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 +297,35 @@ onMounted(() => {
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordCancelOpen"
custom-title="Batalkan Kunjungan"
custom-message="Apakah anda yakin ingin membatalkan kunjungan pasien berikut?"
action="deactivate"
:record="recItem"
@confirm="handleConfirmCancel"
@cancel="handleCancelConfirmation"
>
<template #default="{ record }">
<div class="text-sm">
<p v-if="record?.patient?.person?.name">
<strong>Nama:</strong>
{{ record.patient.person.name }}
</p>
<p v-if="record?.medical_record_number">
<strong>No RM:</strong>
{{ record.medical_record_number }}
</p>
</div>
</template>
</RecordConfirmation>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="handleConfirmDelete"
@cancel="handleCancelConfirmation"
@cancel="handleRemoveConfirmation"
>
<template #default="{ record }">
<div class="text-sm">
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { computed } from 'vue'
// Pubs components
import ContentSwitcher from '~/components/pub/my-ui/content-switcher/content-switcher.vue'
import { useSidebar } from '~/components/pub/ui/sidebar/utils'
// Components
import EncounterPatientInfo from '~/components/app/encounter/quick-info-full.vue'
import EncounterHistoryButtonMenu from '~/components/app/encounter/quick-shortcut.vue'
import SubMenu from '~/components/pub/my-ui/menus/submenu.vue'
// Libraries
import { getPositionAs } from '~/lib/roles'
// Models
import { genEncounter } from '~/models/encounter'
// Types
import type { EncounterProps } from '~/handlers/encounter-init.handler'
// Handlers
import { getEncounterData } from '~/handlers/encounter-process.handler'
import { getMenuItems } from "~/handlers/encounter-init.handler"
const { user, getActiveRole } = useUserStore()
const route = useRoute()
const router = useRouter()
const props = defineProps<{
classCode?: EncounterProps['classCode']
subClassCode?: EncounterProps['subClassCode']
}>()
const activeRole = getActiveRole()
const activePosition = ref(getPositionAs(activeRole))
const menus = ref([] as any)
const activeMenu = computed({
get: () => (route.query?.menu && typeof route.query.menu === 'string' ? route.query.menu : 'status'),
set: (value: string) => {
router.replace({ path: route.path, query: { menu: value } })
},
})
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const data = ref<any>(genEncounter())
const isShowPatient = computed(() => data.value && data.value?.patient?.person)
const { setOpen } = useSidebar()
setOpen(false)
if (activePosition.value === 'none') { // if user position is none, redirect to home page
router.push('/')
}
// Dummy rows for ProtocolList (matches keys expected by list-cfg.protocol)
const protocolRows = [
{
number: '1',
tanggal: new Date().toISOString().substring(0, 10),
siklus: 'I',
periode: 'Siklus I',
kehadiran: 'Hadir',
action: '',
},
{
number: '2',
tanggal: new Date().toISOString().substring(0, 10),
siklus: 'II',
periode: 'Siklus II',
kehadiran: 'Tidak Hadir',
action: '',
},
]
const paginationMeta = {
recordCount: protocolRows.length,
page: 1,
pageSize: 10,
totalPage: 1,
hasNext: false,
hasPrev: false,
}
function handleClick(type: string) {
if (type === 'draft') {
router.back()
}
}
function initMenus() {
menus.value = getMenuItems(id, props, user, {
encounter: data.value
} as any, {
protocolTheraphy: paginationMeta,
protocolChemotherapy: paginationMeta,
medicineProtocolChemotherapy: paginationMeta,
})
}
async function getData() {
data.value = await getEncounterData(id)
}
watch(getActiveRole, () => {
const activeRole = getActiveRole()
activePosition.value = getPositionAs(activeRole)
initMenus()
})
onMounted(async () => {
await getData()
initMenus()
})
</script>
<template>
<div class="w-full">
<div class="mb-4">
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" @click="handleClick" />
</div>
<ContentSwitcher :active="1" :height="200">
<template v-slot:content1>
<EncounterPatientInfo v-if="isShowPatient" :data="data" />
</template>
<template v-slot:content2>
<EncounterHistoryButtonMenu v-if="isShowPatient" />
</template>
</ContentSwitcher>
<SubMenu :data="menus" :initial-active-menu="activeMenu" @change-menu="activeMenu = $event" />
</div>
</template>
+42 -466
View File
@@ -1,483 +1,59 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { onMounted } from 'vue'
// Components
import AppSepEntryForm from '~/components/app/sep/entry-form.vue'
import AppViewPatient from '~/components/app/patient/view-patient.vue'
import AppViewHistory from '~/components/app/sep/view-history.vue'
import AppViewLetter from '~/components/app/sep/view-letter.vue'
import { toast } from '~/components/pub/ui/toast'
// Types
import type { SepHistoryData } from '~/components/app/sep/list-cfg.history'
import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
// Handler
import useIntegrationSepEntry from '~/handlers/integration-sep-entry.handler'
// 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 {
const {
histories,
letters,
patients,
doctors,
diagnoses,
facilitiesFrom,
facilitiesTo,
supportCodesList,
serviceTypesList,
registerMethodsList,
accidentsList,
purposeOfVisitsList,
proceduresList,
assessmentsList,
provincesList,
citiesList,
districtsList,
classLevelsList,
classLevelUpgradesList,
classPaySourcesList,
isServiceHidden,
isSaveLoading,
isLetterReadonly,
isLoadingPatient,
openPatient,
openLetter,
openHistory,
selectedLetter,
selectedObjects,
selectedServiceType,
selectedAdmissionType,
specialistsTree,
selectedPatient,
selectedPatientObject,
paginationMeta,
getLetterMappers,
getPatientsList,
getPatientCurrent,
getPatientByIdentifierSearch,
} from '~/handlers/patient.handler'
const route = useRoute()
const openPatient = ref(false)
const openLetter = ref(false)
const openHistory = ref(false)
const selectedLetter = ref('')
const selectedObjects = ref<any>({})
const selectedServiceType = ref<string>('')
const selectedAdmissionType = ref<string>('')
const histories = ref<Array<SepHistoryData>>([])
const letters = ref<Array<any>>([])
const doctors = ref<Array<{ value: string | number; label: string }>>([])
const diagnoses = ref<Array<{ value: string | number; label: string }>>([])
const facilitiesFrom = ref<Array<{ value: string | number; label: string }>>([])
const facilitiesTo = ref<Array<{ value: string | number; label: string }>>([])
const supportCodesList = ref<Array<{ value: string; label: string }>>([])
const serviceTypesList = ref<Array<{ value: string; label: string }>>([])
const registerMethodsList = ref<Array<{ value: string; label: string }>>([])
const accidentsList = ref<Array<{ value: string; label: string }>>([])
const purposeOfVisitsList = ref<Array<{ value: string; label: string }>>([])
const proceduresList = ref<Array<{ value: string; label: string }>>([])
const assessmentsList = ref<Array<{ value: string; label: string }>>([])
const provincesList = ref<Array<{ value: string; label: string }>>([])
const citiesList = ref<Array<{ value: string; label: string }>>([])
const districtsList = ref<Array<{ value: string; label: string }>>([])
const classLevelsList = ref<Array<{ value: string; label: string }>>([])
const classLevelUpgradesList = ref<Array<{ value: string; label: string }>>([])
const classPaySourcesList = ref<Array<{ value: string; label: string }>>([])
const isServiceHidden = ref(false)
const isSaveLoading = ref(false)
const isLetterReadonly = ref(false)
const specialistsTree = ref<TreeItem[]>([])
const resourceType = ref('')
const resourcePath = ref('')
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 {
// integrate ke sep ---
// "rujukan": {
// "noRujukan": "0212R0300625B000006", // rujukan?.noKunjungan
// "ppkRujukan": "0212R030",
// "tglRujukan": "2025-06-26",
// "asalRujukan": "2" // asalFaskes
// },
// "jnsPelayanan": "2",
// "ppkPelayanan": "1323R001",
// "poli": {
// "tujuan": "URO", // rujukan?.poliRujukan?.kode
// },
// "klsRawat": {
// "pembiayaan": "",
// "klsRawatHak": "2", // peserta.hakKelas?.kode
// "klsRawatNaik": "",
// "penanggungJawab": ""
// },
letters.value = [
{
letterNumber: lettersRaw?.rujukan?.noKunjungan || '',
plannedDate: lettersRaw?.rujukan?.tglKunjungan || '',
sepNumber: lettersRaw?.rujukan?.informasi?.eSEP || '-',
patientName: lettersRaw?.rujukan?.peserta.nama || '',
bpjsCardNo: lettersRaw?.rujukan?.peserta.noKartu || '',
clinic: lettersRaw?.rujukan?.poliRujukan.nama || '',
doctor: '',
information: {
facility: lettersRaw?.asalFaskes || '',
diagnoses: lettersRaw?.rujukan?.diagnosa?.kode || '',
serviceType: lettersRaw?.rujukan?.pelayanan?.kode || '',
classLevel: lettersRaw?.rujukan?.peserta?.hakKelas?.kode || '',
poly: lettersRaw?.rujukan?.poliRujukan?.kode || '',
cardNumber: lettersRaw?.rujukan?.peserta?.noKartu || '',
patientName: lettersRaw?.rujukan?.peserta?.nama || '',
patientPhone: lettersRaw?.rujukan?.peserta?.mr?.noTelepon || '',
medicalRecordNumber: lettersRaw?.rujukan?.peserta?.mr?.noMR || '',
},
},
]
}
}
}
async function getPatientInternalMappers(id: string) {
try {
await getPatientCurrent(id)
if (selectedPatientObject.value) {
const patient = selectedPatientObject.value
selectedObjects.value['cardNumber'] = '-'
selectedObjects.value['nationalIdentity'] = patient?.person?.residentIdentityNumber || '-'
selectedObjects.value['medicalRecordNumber'] = patient?.number || '-'
selectedObjects.value['patientName'] = patient?.person?.name || '-'
selectedObjects.value['phoneNumber'] = patient?.person?.contacts?.[0]?.value || '-'
}
} catch (err) {
console.error('Failed to load patient from query params:', err)
}
}
async function getPatientExternalMappers(id: string, type: string) {
try {
const result = await getMemberList({
mode: type,
number: id,
date: new Date().toISOString().substring(0, 10),
})
if (result && result.success && result.body) {
const memberRaws = result.body?.response || null
selectedObjects.value['cardNumber'] = memberRaws?.peserta?.noKartu || ''
selectedObjects.value['nationalIdentity'] = memberRaws?.peserta?.nik || ''
selectedObjects.value['medicalRecordNumber'] = memberRaws?.peserta?.mr?.noMR || ''
selectedObjects.value['patientName'] = memberRaws?.peserta?.nama || ''
selectedObjects.value['phoneNumber'] = memberRaws?.peserta?.mr?.noTelepon || ''
selectedObjects.value['classLevel'] = memberRaws?.peserta?.hakKelas?.kode || ''
selectedObjects.value['status'] = memberRaws?.statusPeserta?.kode || ''
}
} catch (err) {
console.error('Failed to load patient from query params:', err)
}
}
function handleSaveLetter() {
// 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 || 1,
serviceDate: new Date().toISOString().substring(0, 10),
specialistCode: 0,
})
}
if (menu === 'search-patient') {
getPatientsList({ 'page-size': 10, includes: 'person' }).then(() => {
openPatient.value = true
})
return
}
if (menu === 'search-patient-by-identifier') {
const text = value.text
const type = value.type
const prevCardNumber = selectedPatientObject.value?.person?.residentIdentityNumber || ''
if (type === 'indentity' && text !== prevCardNumber) {
await getPatientByIdentifierSearch(text)
await getPatientExternalMappers(text, 'by-identity')
}
if (type === 'cardNumber' && text !== prevCardNumber) {
await getPatientExternalMappers(text, 'by-card')
}
return
}
if (menu === 'search-letter') {
isLetterReadonly.value = false
getLetterMappers(value.admissionType, value.search).then(() => {
if (letters.value.length > 0) {
const copyObjects = { ...selectedObjects.value }
selectedObjects.value = {}
selectedLetter.value = letters.value[0].letterNumber
isLetterReadonly.value = true
setTimeout(() => {
selectedObjects.value = copyObjects
selectedObjects.value['letterDate'] = letters.value[0].plannedDate
}, 100)
}
})
return
}
if (menu === 'open-letter') {
openLetter.value = true
return
}
if (menu === 'history-sep') {
getMonitoringHistoryMappers().then(() => {
openHistory.value = true
})
return
}
if (menu === 'back') {
navigateTo('/integration/bpjs/sep')
}
if (menu === 'save-sep') {
isSaveLoading.value = true
// value.destinationClinic = value.destinationClinic || ''
createSep(makeSepData(value))
.then((res) => {
const body = res?.body
const code = body?.metaData?.code
const message = body?.metaData?.message
if (code && code !== '200') {
toast({ title: 'Gagal', description: message || 'Gagal membuat SEP', variant: 'destructive' })
return
}
toast({ title: 'Berhasil', description: 'SEP berhasil dibuat', variant: 'default' })
if (resourceType.value === 'encounter') {
navigateTo(resourcePath.value)
return
}
navigateTo('/integration/bpjs/sep')
})
.catch((err) => {
console.error('Failed to save SEP:', err)
toast({ title: 'Gagal', description: err?.message || 'Gagal membuat SEP', variant: 'destructive' })
})
.finally(() => {
isSaveLoading.value = false
})
}
}
async function handleFetch(params: any) {
const menu = params.menu || ''
const value = params.value || ''
if (menu === 'diagnosis') {
diagnoses.value = await getDiagnoseLabelList({ diagnosa: value })
}
if (menu === 'clinic-from') {
facilitiesFrom.value = await getHealthFacilityLabelList({
healthcare: value,
healthcareType: selectedServiceType.value || 1,
})
}
if (menu === 'clinic-to') {
facilitiesTo.value = await getHealthFacilityLabelList({
healthcare: value,
healthcareType: selectedServiceType.value || 1,
})
}
if (menu === 'province') {
citiesList.value = await getCityList({ province: value })
districtsList.value = []
}
if (menu === 'city') {
districtsList.value = await getDistrictList({ city: value })
}
}
async function handleFetchSpecialists() {
try {
const specialistsResult = await getSpecialistList({ 'page-size': 100, includes: 'subspecialists' })
if (specialistsResult.success) {
const specialists = specialistsResult.body?.data || []
specialistsTree.value = getSpecialistTreeItems(specialists)
}
} catch (error) {
console.error('Error fetching specialist-subspecialist tree:', error)
}
}
async function handleInit() {
const facilities = await getHealthFacilityLabelList({
healthcare: 'Puskesmas',
healthcareType: selectedLetter.value || 1,
})
diagnoses.value = await getDiagnoseLabelList({ diagnosa: 'paru' })
facilitiesFrom.value = facilities
facilitiesTo.value = facilities
doctors.value = await getDoctorLabelList({
serviceType: selectedServiceType.value || 1,
serviceDate: new Date().toISOString().substring(0, 10),
specialistCode: 0,
})
provincesList.value = await getProvinceList()
serviceTypesList.value = Object.keys(serviceTypes).map((item) => ({
value: item.toString(),
label: serviceTypes[item],
})) as any
registerMethodsList.value = Object.keys(registerMethods)
.filter((item) => ![''].includes(item))
.map((item) => ({
value: item.toString(),
label: registerMethods[item],
})) as any
accidentsList.value = Object.keys(trafficAccidents).map((item) => ({
value: item.toString(),
label: trafficAccidents[item],
})) as any
purposeOfVisitsList.value = Object.keys(purposeOfVisits).map((item) => ({
value: item.toString(),
label: purposeOfVisits[item],
})) as any
proceduresList.value = Object.keys(procedureTypes).map((item) => ({
value: item.toString(),
label: procedureTypes[item],
})) as any
assessmentsList.value = Object.keys(serviceAssessments).map((item) => ({
value: item.toString(),
label: `${item.toString()} - ${serviceAssessments[item]}`,
})) as any
supportCodesList.value = Object.keys(supportCodes).map((item) => ({
value: item.toString(),
label: `${item.toString()} - ${supportCodes[item]}`,
})) as any
classLevelsList.value = Object.keys(classLevels).map((item) => ({
value: item.toString(),
label: classLevels[item],
})) as any
classLevelUpgradesList.value = Object.keys(classLevelUpgrades).map((item) => ({
value: item.toString(),
label: classLevelUpgrades[item],
})) as any
classPaySourcesList.value = Object.keys(classPaySources).map((item) => ({
value: item.toString(),
label: classPaySources[item],
})) as any
await handleFetchSpecialists()
if (route.query) {
const queries = route.query as any
isServiceHidden.value = queries['is-service'] === 'true'
selectedObjects.value = {}
if (queries['resource']) resourceType.value = queries['resource']
if (queries['resource-path']) resourcePath.value = queries['resource-path']
if (queries['doctor-code']) selectedObjects.value['doctorCode'] = queries['doctor-code']
if (queries['specialist-code']) selectedObjects.value['subSpecialistCode'] = queries['specialist-code']
if (queries['sub-specialist-code']) selectedObjects.value['subSpecialistCode'] = queries['sub-specialist-code']
if (queries['card-number']) selectedObjects.value['cardNumber'] = queries['card-number']
if (queries['register-date']) selectedObjects.value['registerDate'] = queries['register-date']
if (queries['sep-type']) selectedObjects.value['sepType'] = queries['sep-type']
if (queries['sep-number']) selectedObjects.value['sepNumber'] = queries['sep-number']
if (queries['register-date']) selectedObjects.value['registerDate'] = queries['register-date']
if (queries['payment-type']) selectedObjects.value['paymentType'] = queries['payment-type']
if (queries['patient-id']) {
await getPatientInternalMappers(queries['patient-id'])
}
if (queries['card-number']) {
const resultMember = await getMemberList({
mode: 'by-card',
number: queries['card-number'],
date: new Date().toISOString().substring(0, 10),
})
console.log(resultMember)
}
delete selectedObjects.value['is-service']
}
}
handleSaveLetter,
handleSavePatient,
handleEvent,
handleFetch,
handleInit,
} = useIntegrationSepEntry()
onMounted(async () => {
await handleInit()
+77 -184
View File
@@ -1,8 +1,5 @@
<script setup lang="ts">
// Components
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import {
DropdownMenu,
@@ -11,141 +8,48 @@ import {
DropdownMenuItem,
} from '~/components/pub/ui/dropdown-menu'
import AppSepList from '~/components/app/sep/list.vue'
import RangeCalendar from '~/components/pub/ui/range-calendar/RangeCalendar.vue'
// Icons
import { X, Check } from 'lucide-vue-next'
// Types
import type { VclaimSepData } from '~/models/vclaim'
// Libraries
import useIntegrationSepList from '~/handlers/integration-sep-list.handler'
// Services
import { getList as geMonitoringVisitList } from '~/services/vclaim-monitoring-visit.service'
// use handler to provide state and functions
const {
recId,
recAction,
recItem,
data,
dateSelection,
dateRange,
serviceType,
serviceTypesList,
search,
open,
sepData,
headerPrep,
paginationMeta,
isLoading,
getSepList,
setServiceTypes,
setDateRange,
handleExportCsv,
handleExportExcel,
handleRowSelected,
handlePageChange,
handleRemove,
} = useIntegrationSepList()
const search = ref('')
const dateRange = ref('12 Agustus 2025 - 31 Agustus 2025')
const open = ref(false)
const sepData = {
no_sep: 'SP23311224',
kartu: '001234',
nama: 'Kenzie',
}
const paginationMeta = reactive<PaginationMeta>({
recordCount: 0,
page: 1,
pageSize: 10,
totalPage: 5,
hasNext: false,
hasPrev: false,
})
function handlePageChange(page: number) {
console.log('pageChange', page)
}
const data = ref<VclaimSepData[]>([])
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (_val: string) => {
// filter patient list
},
onClear: () => {
// clear url param
},
}
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
})
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const headerPrep: HeaderPrep = {
title: 'Daftar SEP Prosedur',
icon: 'i-lucide-panel-bottom',
addNav: {
label: 'Tambah',
onClick: () => {
navigateTo('/integration/bpjs/sep/add')
},
},
}
async function getMonitoringVisitMappers() {
isLoading.dataListLoading = true
data.value = []
const dateFirst = new Date()
const result = await geMonitoringVisitList({
date: dateFirst.toISOString().substring(0, 10),
serviceType: 1,
})
if (result && result.success && result.body) {
const visitsRaw = result.body?.response?.sep || []
if (!visitsRaw) {
isLoading.dataListLoading = false
return
}
visitsRaw.forEach((result: any) => {
// Format pelayanan: "R.Inap" -> "Rawat Inap", "1" -> "Rawat Jalan", dll
let serviceType = result.jnsPelayanan || '-'
if (serviceType === 'R.Inap') {
serviceType = 'Rawat Inap'
} else if (serviceType === '1' || serviceType === 'R.Jalan') {
serviceType = 'Rawat Jalan'
}
data.value.push({
letterDate: result.tglSep || '-',
letterNumber: result.noSep || '-',
serviceType: serviceType,
flow: '-',
medicalRecordNumber: '-',
patientName: result.nama || '-',
cardNumber: result.noKartu || '-',
controlLetterNumber: result.noRujukan || '-',
controlLetterDate: result.tglPlgSep || '-',
clinicDestination: result.poli || '-',
attendingDoctor: '-',
diagnosis: result.diagnosa || '-',
careClass: result.kelasRawat || '-',
})
})
}
isLoading.dataListLoading = false
}
async function getSepList() {
await getMonitoringVisitMappers()
}
function exportCsv() {
console.log('Ekspor CSV dipilih')
// tambahkan logic untuk generate CSV
}
function exportExcel() {
console.log('Ekspor Excel dipilih')
// tambahkan logic untuk generate Excel
}
function handleDelete() {
console.log('Data dihapus:', sepData)
open.value = false
}
// expose provides so component can also use provide/inject if needed
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
watch(
() => recAction.value,
[recId, recAction],
() => {
if (recAction.value === 'showConfirmDel') {
open.value = true
@@ -153,14 +57,26 @@ watch(
},
)
watch(
() => dateSelection.value,
(val) => {
if (!val) return
setDateRange()
},
{ deep: true },
)
watch(
() => serviceType.value,
() => {
getSepList()
},
)
onMounted(() => {
setServiceTypes()
getSepList()
})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
</script>
<template>
@@ -169,100 +85,77 @@ provide('table_data_loader', isLoading)
<!-- Filter Bar -->
<div class="my-2 flex flex-wrap items-center gap-2">
<!-- Search -->
<Input
v-model="search"
placeholder="Cari No. SEP / No. Kartu BPJS..."
class="w-72"
/>
<Input v-model="search" placeholder="Cari No. SEP / No. Kartu BPJS..." class="w-72" />
<!-- Filter -->
<div class="w-72">
<Select id="serviceType" icon-name="i-lucide-chevron-down" v-model="serviceType" :items="serviceTypesList"
placeholder="Pilih Pelayanan" />
</div>
<!-- Date Range -->
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
class="h-[40px] w-72 border-gray-400 bg-white text-right font-normal"
>
<Button variant="outline" class="h-[40px] w-72 border-gray-400 bg-white text-right font-normal">
{{ dateRange }}
<Icon
name="i-lucide-calendar"
class="h-5 w-5"
/>
<Icon name="i-lucide-calendar" class="h-5 w-5" />
</Button>
</PopoverTrigger>
<PopoverContent class="p-2">
<Calendar mode="range" />
<RangeCalendar v-model="dateSelection" />
</PopoverContent>
</Popover>
<!-- Export -->
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="outline"
class="ml-auto h-[40px] w-[120px] rounded-md border-green-600 text-green-600 hover:bg-green-50"
>
<Icon
name="i-lucide-download"
class="h-5 w-5"
/>
<Button variant="outline"
class="ml-auto h-[40px] w-[120px] rounded-md border-green-600 text-green-600 hover:bg-green-50">
<Icon name="i-lucide-download" class="h-5 w-5" />
Ekspor
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-40">
<DropdownMenuItem @click="exportCsv">Ekspor CSV</DropdownMenuItem>
<DropdownMenuItem @click="exportExcel">Ekspor Excel</DropdownMenuItem>
<DropdownMenuItem @click="handleExportCsv">Ekspor CSV</DropdownMenuItem>
<DropdownMenuItem @click="handleExportExcel">Ekspor Excel</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="mt-4">
<AppSepList
v-if="!isLoading.dataListLoading"
:data="data"
/>
<AppSepList v-if="!isLoading.dataListLoading" :data="data" @update:modelValue="handleRowSelected" />
</div>
<!-- Pagination -->
<template v-if="paginationMeta">
<div v-if="paginationMeta.totalPage > 1">
<PubMyUiPagination
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<PubMyUiPagination :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
<!-- Trigger button -->
<Dialog v-model:open="open">
<DialogTrigger as-child></DialogTrigger>
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>Hapus SEP</DialogTitle>
</DialogHeader>
<DialogDescription class="text-gray-700">Apakah anda yakin ingin menghapus SEP dengan data:</DialogDescription>
<div class="mt-4 space-y-2 text-sm">
<p>No. SEP : {{ sepData.no_sep }}</p>
<p>No. Kartu BPJS : {{ sepData.kartu }}</p>
<p>Nama Pasien : {{ sepData.nama }}</p>
<p><strong>No. SEP:</strong> {{ sepData.sepNumber }}</p>
<p><strong>No. Kartu BPJS:</strong> {{ sepData.cardNumber }}</p>
<p><strong>Nama Pasien:</strong> {{ sepData.patientName }}</p>
</div>
<DialogFooter class="mt-6 flex justify-end gap-3">
<Button
variant="outline"
class="border-green-600 text-green-600 hover:bg-green-50"
@click="open = false"
>
<Button variant="outline" class="border-green-600 text-green-600 hover:bg-green-50" @click="() => {
recId = 0
recAction = ''
open = false
}">
<X class="mr-1 h-4 w-4" />
Tidak
</Button>
<Button
variant="destructive"
class="bg-red-600 hover:bg-red-700"
@click="handleDelete"
>
<Button variant="destructive" class="bg-red-600 hover:bg-red-700" @click="handleRemove">
<Check class="mr-1 h-4 w-4" />
Ya
</Button>
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { type TabItem } from '../comp-tab/type'
const props = defineProps<{
initialActiveMenu: string
data: TabItem[]
}>()
const activeMenu = ref(props.initialActiveMenu)
const emit = defineEmits<{
changeMenu: [value: string]
}>()
function changeMenu(value: string) {
activeMenu.value = value
emit('changeMenu', value)
}
</script>
<template>
<div class="mt-4 flex gap-4">
<!-- Menu Sidebar -->
<div v-if="data.length > 0" class="w-72 flex-shrink-0 rounded-md border bg-white shadow-sm dark:bg-neutral-950">
<div class="max-h-[calc(100vh-12rem)] overflow-y-auto p-2">
<button
v-for="menu in data"
:key="menu.value"
:data-active="activeMenu === menu.value"
class="w-full rounded-md px-4 py-3 text-left text-sm transition-colors data-[active=false]:text-gray-700 data-[active=false]:hover:bg-gray-100 data-[active=true]:bg-primary data-[active=true]:text-white dark:data-[active=false]:text-gray-300 dark:data-[active=false]:hover:bg-neutral-800"
@click="changeMenu(menu.value)"
>
{{ menu.label }}
</button>
</div>
</div>
<!-- Active Menu Content -->
<div v-if="data.find((m) => m.value === activeMenu)?.component" class="flex-1 rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
<component
:is="data.find((m) => m.value === activeMenu)?.component"
v-bind="data.find((m) => m.value === activeMenu)?.props"
/>
</div>
</div>
</template>
@@ -3,5 +3,7 @@ export interface TabItem {
label: string
component?: any
groups?: string[]
classCode?: string[]
subClassCode?: string[]
props?: Record<string, any>
}
@@ -0,0 +1,86 @@
<script setup lang="ts">
const props = defineProps<{
height: 200
activeTab?: 1 | 2
}>()
const activeTab = ref(props.activeTab || 1)
function handleClick(value: 1 | 2) {
activeTab.value = value
}
</script>
<template>
<div class="content-switcher" :style="`height: ${height}px`">
<div :class="`${activeTab === 1 ? 'active' : 'inactive'}`">
<div class="content-wrapper">
<div>
<slot name="content1" />
</div>
</div>
<div class="content-nav">
<button @click="handleClick(1)">
<Icon name="i-lucide-chevron-right" />
</button>
</div>
</div>
<div :class="`${activeTab === 2 ? 'active' : 'inactive'}`">
<div class="content-nav">
<button @click="handleClick(2)">
<Icon name="i-lucide-chevron-left" />
</button>
</div>
<div class="content-wrapper">
<div>
<slot name="content2" />
</div>
</div>
</div>
</div>
</template>
<style>
.content-switcher {
@apply flex overflow-hidden gap-3
}
.content-switcher > * {
@apply border border-slate-300 rounded-md flex overflow-hidden
}
.content-wrapper {
@apply p-4 2xl:p-5 overflow-hidden
}
.inactive .content-wrapper {
@apply p-0 w-0
}
.content-nav {
@apply h-full flex flex-row items-center justify-center content-center !text-2xl overflow-hidden
}
.content-nav button {
@apply pt-2 px-2 h-full w-full
}
/* .content-switcher .inactive > .content-wrapper {
@apply w-0 p-0 opacity-0 transition-all duration-500 ease-in-out
} */
.content-switcher .inactive {
@apply w-16 transition-all duration-500 ease-in-out
}
.content-switcher .inactive > .content-nav {
@apply w-full transition-all duration-100 ease-in-out
}
.content-switcher .active {
@apply grow transition-all duration-500 ease-in-out
}
.content-switcher .active > .content-nav {
@apply w-0 transition-all duration-100 ease-in-out
}
/* .content-switcher .active > .content-wrapper {
@apply w-full delay-1000 transition-all duration-1000 ease-in-out
} */
</style>
+2
View File
@@ -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',
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { type EncounterItem } from "../../../../handlers/encounter-process.handler";
const props = defineProps<{
initialActiveMenu: string
data: EncounterItem[]
}>()
const activeMenu = ref(props.initialActiveMenu)
const emit = defineEmits<{
changeMenu: [value: string]
}>()
function changeMenu(value: string) {
activeMenu.value = value
emit('changeMenu', value)
}
</script>
<template>
<div class="mt-4 flex gap-4">
<!-- Menu Sidebar -->
<div v-if="data.length > 0" class="w-72 flex-shrink-0 rounded-md border bg-white shadow-sm dark:bg-neutral-950">
<div class="max-h-[calc(100vh-12rem)] overflow-y-auto p-2">
<button v-for="menu in data" :key="menu.id" :data-active="activeMenu === menu.id"
class="w-full rounded-md px-4 py-3 text-left text-sm transition-colors data-[active=false]:text-gray-700 data-[active=false]:hover:bg-gray-100 data-[active=true]:bg-primary data-[active=true]:text-white dark:data-[active=false]:text-gray-300 dark:data-[active=false]:hover:bg-neutral-800"
@click="changeMenu(menu.id)">
{{ menu.title }}
</button>
</div>
</div>
<!-- Active Menu Content -->
<div v-if="data.find((m) => m.id === activeMenu)?.component"
class="flex-1 rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
<component :is="data.find((m) => m.id === activeMenu)?.component"
v-bind="data.find((m) => m.id === activeMenu)?.props" />
</div>
</div>
</template>
+2 -2
View File
@@ -17,12 +17,12 @@ export function useRBAC() {
const checkRole = (roleAccess: RoleAccess, _userRoles?: string[]): boolean => {
const roles = authStore.userRole
return roles.some((role: string) => (role in roleAccess) || role === 'system') // system by-passes this check
return roles.some((role: string) => role === 'system' || (role in roleAccess)) // system by-passes this check
}
const checkPermission = (roleAccess: RoleAccess, permission: Permission, _userRoles?: string[]): boolean => {
const roles = authStore.userRole
return roles.some((role: string) => roleAccess[role]?.includes(permission) || role === 'system') // system by-passes this check
return roles.some((role: string) => role === 'system' || roleAccess[role]?.includes(permission)) // system by-passes this check
}
const getUserPermissions = (roleAccess: RoleAccess, _userRoles?: string[]): Permission[] => {
+32
View File
@@ -0,0 +1,32 @@
import type { Permission } from "~/models/role";
// Should we define the keys first?
// export type Keys = 'key1' | 'key2' | 'key3' | etc
export const permissions: Record<string, Record<string, Permission[]>> = {
'/chemo/verification': {
'emp|reg': ['R'],
'emp|doc': ['R'],
'emp|nur': ['R'],
},
'/chemo/verification/[id]': {
'emp|reg': ['R'],
'emp|doc': ['R'],
'emp|nur': ['R'],
},
'/chemo/verification/[id]/process': {
'emp|doc': ['R', 'U'],
'emp|nur': ['R', 'U'],
},
'/chemo/action': {
'emp|reg': ['R', 'U', 'D'],
},
'/chemo/action/[id]': {
'emp|reg': ['R', 'U', 'D'],
},
'/chemo/action/[id]/process': {
'emp|doc': ['R', 'U'],
'emp|nur': ['R', 'U'],
'emp|thr': ['R', 'U'],
},
}
+67
View File
@@ -0,0 +1,67 @@
import type { Permission } from "~/models/role";
// Should we define the keys first?
// export type Keys = 'key1' | 'key2' | 'key3' | etc
export const permissions: Record<string, Record<string, Permission[]>> = {
'/emergency/triage': {
'emp|doc': ['C', 'R', 'U', 'D'],
'emp|nur': ['C', 'R', 'U', 'D'],
},
'/emergency/triage/add': {
'emp|doc': ['C', 'R', 'U', 'D'],
'emp|nur': ['C', 'R', 'U', 'D'],
},
'/emergency/triage/[id]': {
'emp|doc': ['C', 'R', 'U', 'D'],
'emp|nur': ['C', 'R', 'U', 'D'],
},
'/emergency/triage/[id]/edit': {
'emp|doc': ['C', 'R', 'U', 'D'],
'emp|nur': ['C', 'R', 'U', 'D'],
},
'/emergency/encounter': {
'emp|reg': ['C', 'R', 'U', 'D'],
'emp|doc': ['R'],
'emp|nur': ['R'],
'emp|thr': ['R'],
'emp|miw': ['R'],
'emp|nut': ['R'],
'emp|pha': ['R'],
'emp|lab': ['R'],
'emp|rad': ['R'],
},
'/emergency/encounter/add': {
'emp|reg': ['C', 'R', 'U', 'D'],
},
'/emergency/encounter/[id]': {
'emp|reg': ['C', 'R', 'U', 'D'],
'emp|doc': ['R'],
'emp|nur': ['R'],
'emp|thr': ['R'],
'emp|miw': ['R'],
'emp|nut': ['R'],
'emp|pha': ['R'],
'emp|lab': ['R'],
'emp|rad': ['R'],
},
'/emergency/encounter/[id]/edit': {
'emp|reg': ['C', 'R', 'U', 'D'],
},
'/emergency/encounter/[id]/process': {
'emp|doc': ['R', 'U'],
'emp|nur': ['R', 'U'],
'emp|thr': ['R', 'U'],
'emp|miw': ['R', 'U'],
'emp|nut': ['R', 'U'],
'emp|pha': ['R', 'U'],
'emp|lab': ['R', 'U'],
'emp|rad': ['R', 'U'],
},
'/emergency/consulation': {
'emp|doc': ['R'],
},
'/emergency/consulation/[id]/process': {
'emp|doc': ['R', 'U'],
},
}
+25
View File
@@ -0,0 +1,25 @@
import type { Permission } from "~/models/role";
// Should we define the keys first?
// export type Keys = 'key1' | 'key2' | 'key3' | etc
export const permissions: Record<string, Record<string, Permission[]>> = {
'/human-src/employee': {
'div|hrd': ['C', 'R', 'U', 'D'],
},
'/human-src/employee/add': {
'div|hrd': ['C', 'R', 'U', 'D'],
},
'/human-src/employee/[id]/edit': {
'div|hrd': ['C', 'R', 'U', 'D'],
},
'/human-src/intern': {
'div|hrd': ['C', 'R', 'U', 'D'],
},
'/human-src/intern/add': {
'div|hrd': ['C', 'R', 'U', 'D'],
},
'/human-src/intern/[id]/edit': {
'div|hrd': ['C', 'R', 'U', 'D'],
},
}
+57
View File
@@ -0,0 +1,57 @@
import type { Permission } from "~/models/role";
// Should we define the keys first?
// export type Keys = 'key1' | 'key2' | 'key3' | etc
export const permissions: Record<string, Record<string, Permission[]>> = {
'/inpatient/request': {
'emp|reg': ['C', 'R', 'U', 'D'],
},
'/inpatient/request/[id]/detail': {
'emp|reg': ['C', 'R', 'U', 'D'],
},
'/inpatient/encounter': {
'emp|reg': ['C', 'R', 'U', 'D'],
'emp|doc': ['R'],
'emp|nur': ['R'],
'emp|thr': ['R'],
'emp|miw': ['R'],
'emp|nut': ['R'],
'emp|pha': ['R'],
'emp|lab': ['R'],
'emp|rad': ['R'],
},
'/inpatient/encounter/add': {
'emp|reg': ['C', 'R', 'U', 'D'],
},
'/inpatient/encounter/[id]': {
'emp|reg': ['C', 'R', 'U', 'D'],
'emp|doc': ['R'],
'emp|nur': ['R'],
'emp|thr': ['R'],
'emp|miw': ['R'],
'emp|nut': ['R'],
'emp|pha': ['R'],
'emp|lab': ['R'],
'emp|rad': ['R'],
},
'/inpatient/encounter/[id]/edit': {
'emp|reg': ['C', 'R', 'U', 'D'],
},
'/inpatient/encounter/[id]/process': {
'emp|doc': ['R', 'U'],
'emp|nur': ['R', 'U'],
'emp|thr': ['R', 'U'],
'emp|miw': ['R', 'U'],
'emp|nut': ['R', 'U'],
'emp|pha': ['R', 'U'],
'emp|lab': ['R', 'U'],
'emp|rad': ['R', 'U'],
},
'/inpatient/consulation': {
'emp|doc': ['R'],
},
'/inpatient/consulation/[id]/process': {
'emp|doc': ['R', 'U'],
},
}
+19
View File
@@ -0,0 +1,19 @@
import type { Permission } from "~/models/role";
// Should we define the keys first?
// export type Keys = 'key1' | 'key2' | 'key3' | etc
export const permissions: Record<string, Record<string, Permission[]>> = {
'/muc-src/checkup': {
'emp|lab': ['C', 'R', 'U', 'D'],
},
'/muc-src/checkup-category': {
'emp|lab': ['C', 'R', 'U', 'D'],
},
'/mcu-src/antibiotic-src': {
'emp|lab': ['C', 'R', 'U', 'D'],
},
'/mcu-src/antibiotic-src-category': {
'emp|lab': ['C', 'R', 'U', 'D'],
}
}
+31
View File
@@ -0,0 +1,31 @@
import type { Permission } from "~/models/role";
// Should we define the keys first?
// export type Keys = 'key1' | 'key2' | 'key3' | etc
export const permissions: Record<string, Record<string, Permission[]>> = {
'/infra-src/bed': {
'div|fin': ['C', 'R', 'U', 'D'],
},
'/infra-src/chamber': {
'div|fin': ['C', 'R', 'U', 'D'],
},
'/infra-src/room': {
'div|fin': ['C', 'R', 'U', 'D'],
},
'/infra-src/warehouse': {
'div|fin': ['C', 'R', 'U', 'D'],
},
'/infra-src/building': {
'div|fin': ['C', 'R', 'U', 'D'],
},
'/infra-src/floor': {
'div|fin': ['C', 'R', 'U', 'D'],
},
'/infra-src/counter': {
'div|fin': ['C', 'R', 'U', 'D'],
},
'/infra-src/public-screen': {
'div|fin': ['C', 'R', 'U', 'D'],
},
}
+57
View File
@@ -0,0 +1,57 @@
import type { Permission } from "~/models/role";
// Should we define the keys first?
// export type Keys = 'key1' | 'key2' | 'key3' | etc
export const permissions: Record<string, Record<string, Permission[]>> = {
'/outpatient/registration-queue': {
'emp|reg': ['R', 'U', 'D'],
},
'/outpatient/encounter-queue': {
'emp|nur': ['R', 'U', 'D'],
},
'/outpatient/encounter': {
'emp|reg': ['C', 'R', 'U', 'D'],
'emp|doc': ['R'],
'emp|nur': ['R'],
'emp|thr': ['R'],
'emp|miw': ['R'],
'emp|nut': ['R'],
'emp|pha': ['R'],
'emp|lab': ['R'],
'emp|rad': ['R'],
},
'/outpatient/encounter/add': {
'emp|reg': ['C', 'R', 'U', 'D'],
},
'/outpatient/encounter/[id]/detail': {
'emp|reg': ['C', 'R', 'U', 'D'],
'emp|doc': ['R'],
'emp|nur': ['R'],
'emp|thr': ['R'],
'emp|miw': ['R'],
'emp|nut': ['R'],
'emp|pha': ['R'],
'emp|lab': ['R'],
'emp|rad': ['R'],
},
'/outpatient/encounter/[id]/edit': {
'emp|reg': ['C', 'R', 'U', 'D'],
},
'/outpatient/encounter/[id]/process': {
'emp|doc': ['R', 'U'],
'emp|nur': ['R', 'U'],
'emp|thr': ['R', 'U'],
'emp|miw': ['R', 'U'],
'emp|nut': ['R', 'U'],
'emp|pha': ['R', 'U'],
'emp|lab': ['R', 'U'],
'emp|rad': ['R', 'U'],
},
'/outpatient/consulation': {
'emp|doc': ['R'],
},
'/outpatient/consulation/[id]/process': {
'emp|doc': ['R', 'U'],
},
}
@@ -0,0 +1,27 @@
import type { Permission } from "~/models/role";
// Should we define the keys first?
// export type Keys = 'key1' | 'key2' | 'key3' | etc
export const permissions: Record<string, Record<string, Permission[]>> = {
'/tools-equipment-src/medicine': {
'div|fin': ['C', 'R', 'U', 'D'],
'emp|pha': ['R'],
},
'/tools-equipment-src/medicine-method': {
'div|fin': ['C', 'R', 'U', 'D'],
'emp|pha': ['R'],
},
'/tools-equipment-src/medicine-type': {
'div|fin': ['C', 'R', 'U', 'D'],
'emp|pha': ['R'],
},
'/tools-equipment-src/medicine-form': {
'div|fin': ['C', 'R', 'U', 'D'],
'emp|pha': ['R'],
},
'/tools-equipment-src/device-src': {
'div|fin': ['C', 'R', 'U', 'D'],
'emp|pha': ['R'],
},
}
+596
View File
@@ -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<DataTableLoader>({
isTableLoading: false,
})
const paymentsList = ref<Array<{ value: string; label: string }>>([])
const sepsList = ref<Array<{ value: string; label: string }>>([])
const participantGroupsList = ref<Array<{ value: string; label: string }>>([])
const specialistsTree = ref<TreeItem[]>([])
const specialistsData = ref<any[]>([])
const doctorsList = ref<Array<{ value: string; label: string }>>([])
const recSelectId = ref<number | null>(null)
const isSaving = ref(false)
const isLoadingDetail = ref(false)
const encounterData = ref<any>(null)
const formObjects = ref<any>({})
const isSepValid = ref(false)
const isCheckingSep = ref(false)
const sepNumber = ref('')
const vclaimReference = ref<any>(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,
}
}
+489
View File
@@ -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<string, any>
}
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<string, any> = {
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<T>(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<string, Record<string, Record<string, any>>> = {
'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
}
+32
View File
@@ -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
}
@@ -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<any>({})
const selectedServiceType = ref<string>('')
const selectedAdmissionType = ref<string>('')
const histories = ref<Array<SepHistoryData>>([])
const letters = ref<Array<any>>([])
const doctors = ref<Array<{ value: string | number; label: string }>>([])
const diagnoses = ref<Array<{ value: string | number; label: string }>>([])
const facilitiesFrom = ref<Array<{ value: string | number; label: string }>>([])
const facilitiesTo = ref<Array<{ value: string | number; label: string }>>([])
const supportCodesList = ref<Array<{ value: string; label: string }>>([])
const serviceTypesList = ref<Array<{ value: string; label: string }>>([])
const registerMethodsList = ref<Array<{ value: string; label: string }>>([])
const accidentsList = ref<Array<{ value: string; label: string }>>([])
const purposeOfVisitsList = ref<Array<{ value: string; label: string }>>([])
const proceduresList = ref<Array<{ value: string; label: string }>>([])
const assessmentsList = ref<Array<{ value: string; label: string }>>([])
const provincesList = ref<Array<{ value: string; label: string }>>([])
const citiesList = ref<Array<{ value: string; label: string }>>([])
const districtsList = ref<Array<{ value: string; label: string }>>([])
const classLevelsList = ref<Array<{ value: string; label: string }>>([])
const classLevelUpgradesList = ref<Array<{ value: string; label: string }>>([])
const classPaySourcesList = ref<Array<{ value: string; label: string }>>([])
const isServiceHidden = ref(false)
const isSaveLoading = ref(false)
const isLetterReadonly = ref(false)
const isLoadingPatient = ref(false)
const specialistsTree = ref<TreeItem[]>([])
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
@@ -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<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const data = ref<VclaimSepData[]>([])
const dateSelection = ref({ start: initCalDate(today), end: initCalDate(today) }) as VueRef<DateRange>
const dateRange = ref(`${getFormatDateId(today)} - ${getFormatDateId(today)}`)
const serviceType = ref('2')
const serviceTypesList = ref<any[]>([])
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<PaginationMeta>({
recordCount: 0,
page: 1,
pageSize: 10,
totalPage: 5,
hasNext: false,
hasPrev: false,
})
const isLoading = reactive<DataTableLoader>({
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
+1 -1
View File
@@ -8,7 +8,7 @@ export const dataStatusCodes: Record<string, string> = {
review: 'Review',
process: 'Proses',
done: 'Selesai',
canceled: 'Dibatalkan',
cancel: 'Dibatalkan',
rejected: 'Ditolak',
skiped: 'Dilewati',
}
+72 -36
View File
@@ -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}`;
}
}
// 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
}
}
+154
View File
@@ -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<Record<string, any> | 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<string, any>)[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<Record<string, any> | 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<string, any>)[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)
}
+6 -8
View File
@@ -1,7 +1,7 @@
import type { RoleAccess } from '~/models/role'
export const PAGE_PERMISSIONS = {
'/patient': {
'/client/patient': {
'emp|doc': ['R'],
'emp|nur': ['R'],
'emp|reg': ['C', 'R', 'U', 'D'],
@@ -9,13 +9,11 @@ export const PAGE_PERMISSIONS = {
'emp|pay': ['R'],
'emp|mng': ['R'],
},
'/doctor': {
'emp|doc': ['C', 'R', 'U', 'D'],
'emp|nur': ['R'],
'emp|reg': ['R'],
'emp|pha': ['R'],
'emp|pay': ['R'],
'emp|mng': ['R'],
'/human-src/employee': {
'div|hrd': ['C', 'R', 'U', 'D'],
},
'/human-src/intern': {
'div|hrd': ['C', 'R', 'U', 'D'],
},
'/satusehat': {
'emp|doc': ['R'],
+14
View File
@@ -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'
}
+1
View File
@@ -37,6 +37,7 @@ export interface Encounter {
discharge_date?: string
internalReferences?: InternalReference[]
deathCause?: DeathCause
paymentMethod_code?: string
status_code: string
encounterDocuments: EncounterDocument[]
}
@@ -0,0 +1,39 @@
<script setup lang="ts">
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/detail.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['emp|doc', 'emp|nur', 'emp|reg', 'emp|pha', 'emp|pay', 'emp|mng'],
title: 'Detail Kunjungan',
contentFrame: 'cf-full-width',
})
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => `${route.meta.title}`,
})
</script>
<template>
<div v-if="canRead">
<Content />
</div>
<Error v-else :status-code="403" />
</template>
@@ -0,0 +1,39 @@
<script setup lang="ts">
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-next.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['emp|doc', 'emp|nur', 'emp|reg', 'emp|pha', 'emp|pay', 'emp|mng'],
title: 'Tambah Kunjungan',
contentFrame: 'cf-full-width',
})
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => `${route.meta.title}`,
})
</script>
<template>
<div v-if="canRead">
<Content class-code="ambulatory" sub-class-code="chemo" />
</div>
<Error v-else :status-code="403" />
</template>
@@ -1,44 +1,46 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import Content from '~/components/content/encounter/entry.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
roles: ['emp|reg'],
title: 'Tambah Kunjungan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const { checkRole, hasCreateAccess } = useRBAC()
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess, hasCreateAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
throw createError({
statusCode: 403,
statusMessage: 'Access denied',
})
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
// Define permission-based computed properties
const canCreate = hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentEncounterEntry
<Content
:id="0"
class-code="ambulatory"
sub-class-code="rehab"
sub-class-code="chemo"
form-type="Tambah"
/>
</div>
@@ -1,41 +1,43 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import Content from '~/components/content/encounter/list.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['system', 'emp-doc', 'emp-nur', 'emp-reg', 'emp-pha', 'emp-pay', 'emp-mng'],
roles: ['emp|reg', 'emp|nur', 'emp|doc', 'emp|miw', 'emp|thr', 'emp|nut', 'emp|pha', 'emp|lab'],
title: 'Daftar Kunjungan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = true // hasReadAccess(roleAccess)
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
</script>
<template>
<div>
<div v-if="canRead">
<ContentEncounterList
<Content
class-code="ambulatory"
sub-class-code="rehab"
sub-class-code="chemo"
type="encounter"
/>
</div>
@@ -1,10 +0,0 @@
<script setup lang="ts">
const route = useRoute();
</script>
<template>
<div class="p-10 text-center">
<div class="mb-5 text-base font-semibold">Hello world!!</div>
<div>You are accessing "{{ route.fullPath }}"</div>
</div>
</template>
@@ -1,10 +0,0 @@
<script setup lang="ts">
const route = useRoute();
</script>
<template>
<div class="p-10 text-center">
<div class="mb-5 text-base font-semibold">Hello world!!</div>
<div>You are accessing "{{ route.fullPath }}"</div>
</div>
</template>
-41
View File
@@ -1,41 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah Dokter',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor']
const { checkRole, hasCreateAccess } = 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 canCreate = hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentDoctorAdd />
</div>
<Error v-else :status-code="403" />
</template>
@@ -0,0 +1,39 @@
<script setup lang="ts">
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/emergency'
import Error from '~/components/pub/my-ui/error/error.vue'
import Content from '~/components/content/encounter/detail.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['emp|doc', 'emp|nur', 'emp|reg', 'emp|pha', 'emp|pay', 'emp|mng'],
title: 'Detail Kunjungan',
contentFrame: 'cf-full-width',
})
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/emergency/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => `${route.meta.title}`,
})
</script>
<template>
<div v-if="canRead">
<Content />
</div>
<Error v-else :status-code="403" />
</template>
@@ -1,23 +1,18 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/emergency'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import Content from '~/components/content/encounter/process-next.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Daftar Pasien',
title: 'Kunjungan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/emergency/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
@@ -28,13 +23,17 @@ if (!hasAccess) {
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => `${route.meta.title}`,
})
</script>
<template>
<div>
<div v-if="canRead">
<ContentPatientList />
</div>
<Error v-else :status-code="403" />
<div v-if="canRead">
<Content class-code="emergency" sub-class-code="emg" />
</div>
</template>
<Error v-else :status-code="403" />
</template>
@@ -1,44 +1,46 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import Content from '~/components/content/encounter/entry.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
roles: ['emp|reg'],
title: 'Tambah Kunjungan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/emergency/encounter']
const { checkRole, hasCreateAccess } = useRBAC()
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess, hasCreateAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
throw createError({
statusCode: 403,
statusMessage: 'Access denied',
})
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
// Define permission-based computed properties
const canCreate = hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentEncounterEntry
<Content
:id="0"
class-code="emergency"
sub-class-code="emg"
sub-class-code="reg"
form-type="Tambah"
/>
</div>
@@ -1,23 +1,19 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import Content from '~/components/content/encounter/list.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['system', 'doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
roles: ['emp|reg', 'emp|nur', 'emp|doc', 'emp|miw', 'emp|thr', 'emp|nut', 'emp|pha', 'emp|lab'],
title: 'Daftar Kunjungan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/emergency/encounter']
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
@@ -28,14 +24,28 @@ if (!hasAccess) {
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const { user, getActiveRole } = useUserStore()
// const activeRole = getActiveRole()
// const activeRoleParts = activeRole ? activeRole.split('|') : ['', '']
// const activeRolePos = activeRoleParts[0] // reaching this page means it is already set
// const activeRoleType = activeRoleParts[1] == 'rehab' ? activeRoleParts[1] : ''
const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg'
</script>
<template>
<div>
<div v-if="canRead">
<ContentEncounterList
<Content
class-code="emergency"
sub-class-code="emg"
sub-class-code="reg"
type="encounter"
/>
</div>
@@ -0,0 +1,3 @@
<template>
</template>
@@ -0,0 +1,3 @@
<template>
</template>
@@ -0,0 +1,39 @@
<script setup lang="ts">
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/inpatient'
import Error from '~/components/pub/my-ui/error/error.vue'
import Content from '~/components/content/encounter/detail.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['emp|doc', 'emp|nur', 'emp|reg', 'emp|pha', 'emp|pay', 'emp|mng'],
title: 'Detail Kunjungan',
contentFrame: 'cf-full-width',
})
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/inpatient/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => `${route.meta.title}`,
})
</script>
<template>
<div v-if="canRead">
<Content />
</div>
<Error v-else :status-code="403" />
</template>
@@ -1,23 +1,18 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/inpatient'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import Content from '~/components/content/encounter/process-next.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Daftar Dokter',
title: 'Kunjungan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor']
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/inpatient/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
@@ -28,13 +23,17 @@ if (!hasAccess) {
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => `${route.meta.title}`,
})
</script>
<template>
<div>
<div v-if="canRead">
<ContentDoctorList />
</div>
<Error v-else :status-code="403" />
<div v-if="canRead">
<Content class-code="inpatient" sub-class-code="vk" />
</div>
<Error v-else :status-code="403" />
</template>
@@ -1,44 +1,46 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import Content from '~/components/content/encounter/entry.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
roles: ['emp|reg'],
title: 'Tambah Kunjungan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/inpatient/encounter']
const { checkRole, hasCreateAccess } = useRBAC()
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess, hasCreateAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
throw createError({
statusCode: 403,
statusMessage: 'Access denied',
})
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
// Define permission-based computed properties
const canCreate = hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentEncounterEntry
<Content
:id="0"
class-code="inpatient"
sub-class-code="icu"
class-code="ambulatory"
sub-class-code="reg"
form-type="Tambah"
/>
</div>
@@ -1,23 +1,19 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import Content from '~/components/content/encounter/list.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['system', 'doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
roles: ['emp|reg', 'emp|nur', 'emp|doc', 'emp|miw', 'emp|thr', 'emp|nut', 'emp|pha', 'emp|lab'],
title: 'Daftar Kunjungan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/inpatient/encounter']
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
@@ -28,14 +24,28 @@ if (!hasAccess) {
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const { user, getActiveRole } = useUserStore()
// const activeRole = getActiveRole()
// const activeRoleParts = activeRole ? activeRole.split('|') : ['', '']
// const activeRolePos = activeRoleParts[0] // reaching this page means it is already set
// const activeRoleType = activeRoleParts[1] == 'rehab' ? activeRoleParts[1] : ''
const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg'
</script>
<template>
<div>
<div v-if="canRead">
<ContentEncounterList
<Content
class-code="inpatient"
sub-class-code="vk"
:sub-class-code="subClassCode"
type="encounter"
/>
</div>
@@ -0,0 +1,39 @@
<script setup lang="ts">
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/detail.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['emp|doc', 'emp|nur', 'emp|reg', 'emp|pha', 'emp|pay', 'emp|mng'],
title: 'Detail Kunjungan',
contentFrame: 'cf-full-width',
})
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => `${route.meta.title}`,
})
</script>
<template>
<div v-if="canRead">
<Content />
</div>
<Error v-else :status-code="403" />
</template>
@@ -0,0 +1,39 @@
<script setup lang="ts">
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-next.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['emp|doc', 'emp|nur', 'emp|reg', 'emp|pha', 'emp|pay', 'emp|mng'],
title: 'Tambah Kunjungan',
contentFrame: 'cf-full-width',
})
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => `${route.meta.title}`,
})
</script>
<template>
<div v-if="canRead">
<Content class-code="ambulatory" sub-class-code="reg" />
</div>
<Error v-else :status-code="403" />
</template>
@@ -1,41 +1,43 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import Content from '~/components/content/encounter/entry.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
roles: ['emp|reg'],
title: 'Tambah Kunjungan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/outpatient/encounter']
const { checkRole, hasCreateAccess } = useRBAC()
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess, hasCreateAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
throw createError({
statusCode: 403,
statusMessage: 'Access denied',
})
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
// Define permission-based computed properties
const canCreate = hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentEncounterEntry
<Content
:id="0"
class-code="ambulatory"
sub-class-code="reg"
@@ -1,23 +1,19 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import Content from '~/components/content/encounter/list.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['system', 'doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
roles: ['emp|reg', 'emp|nur', 'emp|doc', 'emp|miw', 'emp|thr', 'emp|nut', 'emp|pha', 'emp|lab'],
title: 'Daftar Kunjungan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/outpatient/encounter']
// Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
@@ -28,14 +24,28 @@ if (!hasAccess) {
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const { user, getActiveRole } = useUserStore()
// const activeRole = getActiveRole()
// const activeRoleParts = activeRole ? activeRole.split('|') : ['', '']
// const activeRolePos = activeRoleParts[0] // reaching this page means it is already set
// const activeRoleType = activeRoleParts[1] == 'rehab' ? activeRoleParts[1] : ''
const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg'
</script>
<template>
<div>
<div v-if="canRead">
<ContentEncounterList
<Content
class-code="ambulatory"
sub-class-code="reg"
:sub-class-code="subClassCode"
type="encounter"
/>
</div>
@@ -1,10 +0,0 @@
<script setup lang="ts">
const route = useRoute();
</script>
<template>
<div class="p-10 text-center">
<div class="mb-5 text-base font-semibold">Hello world!!</div>
<div>You are accessing "{{ route.fullPath }}"</div>
</div>
</template>
@@ -1,47 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import ContentChemotherapyAdminList from '~/components/content/chemotherapy/admin-list.vue'
import ContentChemotherapyVerification from '~/components/content/chemotherapy/verification.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Kemoterapi Admin',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => 'Verifikasi Jadwal Pasien',
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = true // hasReadAccess(roleAccess)
const mode = computed(() => route.params.mode as string)
</script>
<template>
<div>
<div v-if="canRead">
<ContentChemotherapyVerification />
</div>
<Error
v-else
:status-code="403"
/>
</div>
</template>
@@ -1,55 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import ContentChemotherapyAdminList from '~/components/content/chemotherapy/admin-list.vue'
import ContentChemotherapyVerification from '~/components/content/chemotherapy/verification.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Kemoterapi Admin',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => {
const mode = route.params.mode as string
if (mode === 'admin') {
return 'Administrasi Pasien Rawat Jalan Kemoterapi'
} else if (mode === 'series') {
return 'Proses Jadwal Pasien'
}
return route.meta.title as string
},
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = true // hasReadAccess(roleAccess)
const mode = computed(() => route.params.mode as string)
</script>
<template>
<div>
<div v-if="canRead">
<ContentChemotherapyAdminList v-if="mode === 'admin'" />
<ContentChemotherapyProcess v-else-if="mode === 'series'" />
<Error v-else :status-code="404" status-message="Mode tidak ditemukan" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -1,9 +0,0 @@
<script setup lang="ts">
// Redirect to list page - the process page is now at [id]/index.vue
const route = useRoute()
navigateTo('/outpation-action/chemotherapy/list')
</script>
<template>
<div />
</template>
@@ -1,40 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Daftar Kempterapi',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = true // hasReadAccess(roleAccess)
</script>
<template>
<div>
<div v-if="canRead">
<ContentChemotherapyList />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -1,10 +0,0 @@
<script setup lang="ts">
const route = useRoute();
</script>
<template>
<div class="p-10 text-center">
<div class="mb-5 text-base font-semibold">Hello world!!</div>
<div>You are accessing "{{ route.fullPath }}"</div>
</div>
</template>
@@ -1,9 +0,0 @@
<script setup lang="ts">
definePageMeta({
roles: ['sys', 'doc'],
})
</script>
<template>
<div>detail pasien</div>
</template>
@@ -1,9 +0,0 @@
<script setup lang="ts">
definePageMeta({
roles: ['sys', 'doc'],
})
</script>
<template>
<div>edit pasien</div>
</template>
-41
View File
@@ -1,41 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah Pasien',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasCreateAccess } = 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 canCreate = hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentPatientAdd />
</div>
<Error v-else :status-code="403" />
</template>
@@ -1,3 +0,0 @@
<template>
<div>Examination Queue</div>
</template>
@@ -1,41 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Update Surat Kontrol',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
// const canRead = hasReadAccess(roleAccess)
const canRead = true
</script>
<template>
<div>
<div v-if="canRead">
<ContentControlLetterEdit />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -1,41 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Detail Surat Kontrol',
contentFrame: 'cf-container-md',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
// const canRead = hasReadAccess(roleAccess)
const canRead = true
</script>
<template>
<div>
<div v-if="canRead">
<ContentControlLetterDetail :patient-id="Number(route.params.id)" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -1,41 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah Surat Kontrol',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const { checkRole, getPagePermissions } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
const pagePermission = getPagePermissions(roleAccess)
const callbackUrl = route.query['return-path'] as string | undefined
</script>
<template>
<div>
<div v-if="pagePermission.canRead">
<ContentControlLetterAdd :callback-url="callbackUrl" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -1,41 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
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}`, // backtick to avoid the ts-plugin(2322) warning
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const { checkRole, hasCreateAccess } = 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 canCreate = hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentEncounterEntry :id="1" form-type="Detail" />
</div>
<Error v-else :status-code="403" />
</template>
@@ -1,41 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Update Dokumen Pendukung',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
// const canRead = hasReadAccess(roleAccess)
const canRead = true
</script>
<template>
<div>
<div v-if="canRead">
<ContentDocumentUploadEdit/>
</div>
<Error v-else :status-code="403" />
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More