Merge branch 'dev' into feat/kfr-kemoterapi-174
This commit is contained in:
@@ -0,0 +1,146 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Block from '~/components/pub/my-ui/doc-entry/block.vue'
|
||||||
|
import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
|
||||||
|
import Field from '~/components/pub/my-ui/doc-entry/field.vue'
|
||||||
|
import Label from '~/components/pub/my-ui/doc-entry/label.vue'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import type z from 'zod'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { genBase } from '~/models/_base'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: any
|
||||||
|
schema: z.ZodSchema<any>
|
||||||
|
excludeFields?: string[]
|
||||||
|
isReadonly?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', val: any): void
|
||||||
|
(e: 'submit', val: any): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Setup form
|
||||||
|
const {
|
||||||
|
validate: _validate,
|
||||||
|
defineField,
|
||||||
|
handleSubmit,
|
||||||
|
errors,
|
||||||
|
values,
|
||||||
|
} = useForm({
|
||||||
|
validationSchema: toTypedSchema(props.schema),
|
||||||
|
initialValues: props.modelValue,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(values, (val) => emit('update:modelValue', val), { deep: true })
|
||||||
|
|
||||||
|
const [subjective, subjectiveAttrs] = defineField('subjective')
|
||||||
|
const [objective, objectiveAttrs] = defineField('objective')
|
||||||
|
const [assesment, assesmentAttrs] = defineField('assesment')
|
||||||
|
const [plan, planAttrs] = defineField('plan')
|
||||||
|
const [review, reviewAttrs] = defineField('review')
|
||||||
|
|
||||||
|
const validate = async () => {
|
||||||
|
const result = await _validate()
|
||||||
|
console.log('Component validate() result:', result)
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
data: result.values,
|
||||||
|
errors: result.errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ validate })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form id="entry-form">
|
||||||
|
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||||
|
<div class="my-2">
|
||||||
|
<h1 class="font-semibold">Data Petugas</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-2 rounded-md border border-slate-300 p-4">
|
||||||
|
<Block :colCount="2">
|
||||||
|
<Cell>
|
||||||
|
<Label dynamic>PPA</Label>
|
||||||
|
<Field>
|
||||||
|
<Input disabled />
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
|
||||||
|
<Cell>
|
||||||
|
<Label dynamic>Nama PPA</Label>
|
||||||
|
<Field>
|
||||||
|
<Input disabled />
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-2">
|
||||||
|
<h1 class="font-semibold">Data S.O.A.P</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-2 rounded-md border border-slate-300 p-4">
|
||||||
|
<Block :colCount="2">
|
||||||
|
<Cell>
|
||||||
|
<Label dynamic>Subjektif</Label>
|
||||||
|
<Field>
|
||||||
|
<Textarea
|
||||||
|
v-model="subjective"
|
||||||
|
v-bind="subjectiveAttrs"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
|
||||||
|
<Cell>
|
||||||
|
<Label dynamic>Objektif</Label>
|
||||||
|
<Field>
|
||||||
|
<Textarea
|
||||||
|
v-model="objective"
|
||||||
|
v-bind="objectiveAttrs"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
<Block :colCount="2">
|
||||||
|
<Cell>
|
||||||
|
<Label dynamic>Assesmen</Label>
|
||||||
|
<Field>
|
||||||
|
<Textarea
|
||||||
|
v-model="assesment"
|
||||||
|
v-bind="assesmentAttrs"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
|
||||||
|
<Cell>
|
||||||
|
<Label dynamic>Plan</Label>
|
||||||
|
<Field>
|
||||||
|
<Textarea
|
||||||
|
v-model="plan"
|
||||||
|
v-bind="planAttrs"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
<Block>
|
||||||
|
<Cell>
|
||||||
|
<Label dynamic>Review</Label>
|
||||||
|
<Field>
|
||||||
|
<Textarea
|
||||||
|
v-model="review"
|
||||||
|
v-bind="reviewAttrs"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
<Separator class="mt-8" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { Config, RecComponent, RecStrFuncComponent, RecStrFuncUnknown } from '~/components/pub/my-ui/data-table'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
import type { GeneralConsent } from '~/models/general-consent'
|
||||||
|
|
||||||
|
type SmallDetailDto = any
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{ width: 100 }, {}, {}, {}, { width: 50 }],
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'Tanggal' },
|
||||||
|
{ label: 'PPA' },
|
||||||
|
{ label: 'Hasil' },
|
||||||
|
{ label: 'Review & Verifikasi' },
|
||||||
|
{ label: 'Status' },
|
||||||
|
{ label: 'Aksi' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
keys: ['date', 'ppa', 'result', 'review', 'status', 'action'],
|
||||||
|
delKeyNames: [
|
||||||
|
{ key: 'data', label: 'Tanggal' },
|
||||||
|
{ key: 'dstDoctor.name', label: 'Dokter' },
|
||||||
|
],
|
||||||
|
parses: {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
props: {
|
||||||
|
size: 'sm',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
date(rec) {
|
||||||
|
const recX = rec as GeneralConsent
|
||||||
|
return recX.date?.substring(0, 10) || '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
props: {
|
||||||
|
size: 'sm',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
} as RecStrFuncComponent,
|
||||||
|
htmls: {} as RecStrFuncUnknown,
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||||
|
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||||
|
import { config } from './list.cfg'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
:skeleton-size="paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
<!-- FIXME: pindahkan ke content/division/list.vue -->
|
||||||
|
<PaginationView
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<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 activeServicePosition = inject<Ref<string>>('activeServicePosition')! // previously: activePosition
|
||||||
|
|
||||||
|
const activeKey = ref<string | null>(null)
|
||||||
|
const linkItemsFiltered = ref<LinkItem[]>([])
|
||||||
|
const baseLinkItems: LinkItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Detail',
|
||||||
|
value: 'detail',
|
||||||
|
onClick: () => {
|
||||||
|
proceedItem(ActionEvents.showDetail)
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-eye',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const medicalLinkItems: LinkItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Process',
|
||||||
|
value: 'process',
|
||||||
|
onClick: () => {
|
||||||
|
proceedItem(ActionEvents.showProcess)
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-shuffle',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const regLinkItems: LinkItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Print',
|
||||||
|
value: 'print',
|
||||||
|
onClick: () => {
|
||||||
|
proceedItem(ActionEvents.showPrint)
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-printer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Batalkan',
|
||||||
|
value: 'cancel',
|
||||||
|
onClick: () => {
|
||||||
|
proceedItem(ActionEvents.showCancel)
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-circle-x',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hapus',
|
||||||
|
value: 'remove',
|
||||||
|
onClick: () => {
|
||||||
|
proceedItem(ActionEvents.showConfirmDelete)
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-trash',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const voidLinkItems: LinkItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Nothing',
|
||||||
|
value: 'nothing',
|
||||||
|
icon: 'i-lucide-file',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
linkItemsFiltered.value = [...baseLinkItems]
|
||||||
|
|
||||||
|
getLinks()
|
||||||
|
|
||||||
|
watch(activeServicePosition, () => {
|
||||||
|
getLinks()
|
||||||
|
})
|
||||||
|
|
||||||
|
function proceedItem(action: string) {
|
||||||
|
recId.value = props.rec.id || 0
|
||||||
|
recItem.value = props.rec
|
||||||
|
recAction.value = action
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLinks() {
|
||||||
|
switch (activeServicePosition.value) {
|
||||||
|
case 'medical':
|
||||||
|
linkItemsFiltered.value = [...baseLinkItems, ...medicalLinkItems]
|
||||||
|
break
|
||||||
|
case 'registration':
|
||||||
|
linkItemsFiltered.value = [...baseLinkItems, ...regLinkItems]
|
||||||
|
case 'unit|resp':
|
||||||
|
linkItemsFiltered.value = [...baseLinkItems]
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
linkItemsFiltered.value = voidLinkItems
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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>
|
||||||
@@ -20,6 +20,7 @@ import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
|
|||||||
// Helpers
|
// Helpers
|
||||||
import { toTypedSchema } from '@vee-validate/zod'
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
import { useForm } from 'vee-validate'
|
import { useForm } from 'vee-validate'
|
||||||
|
import { refDebounced } from '@vueuse/core'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
@@ -92,8 +93,9 @@ watch(subSpecialistId, async (newValue) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch SEP number changes to notify parent
|
// Debounced SEP number watcher: emit change only after user stops typing
|
||||||
watch(sepNumber, (newValue) => {
|
const debouncedSepNumber = refDebounced(sepNumber, 500)
|
||||||
|
watch(debouncedSepNumber, (newValue) => {
|
||||||
emit('event', 'sep-number-changed', newValue)
|
emit('event', 'sep-number-changed', newValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -452,7 +454,11 @@ defineExpose({
|
|||||||
name="i-lucide-loader-2"
|
name="i-lucide-loader-2"
|
||||||
class="h-4 w-4 animate-spin"
|
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>
|
||||||
<Button
|
<Button
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Calendar as CalendarIcon, Filter as FilterIcon, Search } from 'lucide-vue-next'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { DateRange } from 'radix-vue'
|
||||||
|
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import type { RefExportNav, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
refSearchNav?: RefSearchNav
|
||||||
|
enableExport?: boolean
|
||||||
|
refExportNav?: RefExportNav
|
||||||
|
onFilterClick?: () => void
|
||||||
|
onExportPdf?: () => void
|
||||||
|
onExportExcel?: () => void
|
||||||
|
onExportCsv?: () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// function emitSearchNavClick() {
|
||||||
|
// props.refSearchNav?.onClick()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// function onInput(event: Event) {
|
||||||
|
// props.refSearchNav?.onInput((event.target as HTMLInputElement).value)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// function btnClick() {
|
||||||
|
// props.prep?.addNav?.onClick?.()
|
||||||
|
// }
|
||||||
|
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const dateRange = ref<{ from: Date | null; to: Date | null }>({
|
||||||
|
from: new Date(),
|
||||||
|
to: new Date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const df = new DateFormatter('en-US', {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get current date
|
||||||
|
const today = new Date()
|
||||||
|
const todayCalendar = new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate())
|
||||||
|
|
||||||
|
// Get date 1 month ago
|
||||||
|
const oneMonthAgo = new Date(today)
|
||||||
|
oneMonthAgo.setMonth(today.getMonth() - 1)
|
||||||
|
const oneMonthAgoCalendar = new CalendarDate(oneMonthAgo.getFullYear(), oneMonthAgo.getMonth() + 1, oneMonthAgo.getDate())
|
||||||
|
|
||||||
|
const value = ref({
|
||||||
|
start: oneMonthAgoCalendar,
|
||||||
|
end: todayCalendar,
|
||||||
|
}) as Ref<DateRange>
|
||||||
|
|
||||||
|
// function onFilterClick() {
|
||||||
|
// console.log('Search:', searchQuery.value)
|
||||||
|
// console.log('Date Range:', dateRange.value)
|
||||||
|
// props.refSearchNav?.onClick()
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative w-64">
|
||||||
|
<Search class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-gray-400" />
|
||||||
|
<Input v-model="searchQuery" type="text" placeholder="Cari Nama /No.RM" class="pl-9" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
:class="cn('w-[200px] justify-start text-left font-normal', !value && 'text-muted-foreground')"
|
||||||
|
>
|
||||||
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||||
|
<template v-if="value.start">
|
||||||
|
<template v-if="value.end">
|
||||||
|
{{ df.format(value.start.toDate(getLocalTimeZone())) }} -
|
||||||
|
{{ df.format(value.end.toDate(getLocalTimeZone())) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
{{ df.format(value.start.toDate(getLocalTimeZone())) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else> Pick a date </template>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-auto p-0">
|
||||||
|
<RangeCalendar
|
||||||
|
v-model="value"
|
||||||
|
initial-focus
|
||||||
|
:number-of-months="2"
|
||||||
|
@update:start-value="(startDate) => (value.start = startDate)"
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Button variant="outline" class="border-orange-500 text-orange-600 hover:bg-orange-50" @click="onFilterClick">
|
||||||
|
<FilterIcon class="mr-2 size-4" />
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DropdownMenu v-show="props.enableExport">
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<Button variant="outline" class="ml-auto border-orange-500 text-orange-600 hover:bg-orange-50">
|
||||||
|
<Icon name="i-lucide-download" class="h-4 w-4" />
|
||||||
|
Ekspor
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem @click="onExportPdf">
|
||||||
|
Ekspor PDF
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="onExportCsv">
|
||||||
|
Ekspor CSV
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="onExportExcel">
|
||||||
|
Ekspor Excel
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</template>
|
||||||
@@ -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>
|
||||||
@@ -6,7 +6,7 @@ import { getAge } from '~/lib/date'
|
|||||||
|
|
||||||
type SmallDetailDto = Encounter
|
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'))
|
const statusBadge = defineAsyncComponent(() => import('./status-badge.vue'))
|
||||||
|
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import DataTable from '~/components/pub/my-ui/data-table/data-table.vue'
|
||||||
import { config } from './list.cfg'
|
import { config } from './list.cfg'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: any[]
|
data: any[],
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PubMyUiDataTable
|
<DataTable
|
||||||
v-bind="config"
|
v-bind="config"
|
||||||
:rows="data"
|
:rows="props.data"
|
||||||
/>
|
/>
|
||||||
</template>
|
</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>
|
||||||
@@ -1,52 +1,52 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
import { genderCodes } from '~/const/key-val/person';
|
||||||
import type { Encounter } from '~/models/encounter'
|
import type { Encounter } from '~/models/encounter'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: Encounter
|
data: Encounter
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
let address = ''
|
let address = ref('')
|
||||||
if (props.data.patient.person.addresses) {
|
if (props.data.patient.person.addresses) {
|
||||||
address = props.data.patient.person.addresses.map((a) => a.address).join(', ')
|
address.value = props.data.patient.person.addresses.map((a) => a.address).join(', ')
|
||||||
}
|
}
|
||||||
|
|
||||||
let dpjp = ''
|
let dpjp = ref('')
|
||||||
if (props.data.responsible_doctor) {
|
if (props.data.responsible_doctor) {
|
||||||
const dp = props.data.responsible_doctor.employee.person
|
const dp = props.data.responsible_doctor.employee.person
|
||||||
dpjp = `${dp.frontTitle} ${dp.name} ${dp.endTitle}`
|
dpjp.value = `${dp.frontTitle} ${dp.name} ${dp.endTitle}`
|
||||||
} else if (props.data.appointment_doctor) {
|
} else if (props.data.appointment_doctor) {
|
||||||
dpjp = props.data.appointment_doctor.employee.person.name
|
dpjp.value = props.data.appointment_doctor.employee.person.name
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
|
<div class="w-full">
|
||||||
<!-- Data Pasien -->
|
<!-- Data Pasien -->
|
||||||
<h2 class="mb-2 font-semibold md:text-base 2xl:text-lg">
|
<h2 class="mb-2 font-semibold md:text-base 2xl:text-lg">
|
||||||
{{ data.patient.person.name }} - {{ data.patient.number }}
|
{{ data.patient.person.name }} - {{ data.patient.number }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-3">
|
<div class="grid grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<DE.Block
|
<DE.Block mode="preview">
|
||||||
mode="preview"
|
|
||||||
labelSize="large"
|
|
||||||
>
|
|
||||||
<DE.Cell>
|
<DE.Cell>
|
||||||
<DE.Label class="font-semibold">No. RM</DE.Label>
|
<DE.Label class="font-semibold">No. RM</DE.Label>
|
||||||
|
<DE.Colon />
|
||||||
<DE.Field>
|
<DE.Field>
|
||||||
{{ data.patient.person.birthDate?.substring(0, 10) }}
|
{{ data.patient.person.birthDate?.substring(0, 10) }}
|
||||||
</DE.Field>
|
</DE.Field>
|
||||||
</DE.Cell>
|
</DE.Cell>
|
||||||
<DE.Cell>
|
<DE.Cell>
|
||||||
<DE.Label class="font-semibold">Jenis Kelamin</DE.Label>
|
<DE.Label class="font-semibold">Jns. Kelamin</DE.Label>
|
||||||
|
<DE.Colon />
|
||||||
<DE.Field>
|
<DE.Field>
|
||||||
{{ data.patient.person.gender_code }}
|
{{ genderCodes[data.patient.person.gender_code as keyof typeof genderCodes] }}
|
||||||
</DE.Field>
|
</DE.Field>
|
||||||
</DE.Cell>
|
</DE.Cell>
|
||||||
<DE.Cell>
|
<DE.Cell>
|
||||||
<DE.Label class="font-semibold">Alamat</DE.Label>
|
<DE.Label class="font-semibold">Alamat</DE.Label>
|
||||||
|
<DE.Colon />
|
||||||
<DE.Field>
|
<DE.Field>
|
||||||
<div v-html="address"></div>
|
<div v-html="address"></div>
|
||||||
</DE.Field>
|
</DE.Field>
|
||||||
@@ -54,35 +54,39 @@ if (props.data.responsible_doctor) {
|
|||||||
</DE.Block>
|
</DE.Block>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<DE.Block
|
<DE.Block mode="preview">
|
||||||
mode="preview"
|
|
||||||
labelSize="large"
|
|
||||||
>
|
|
||||||
<DE.Cell>
|
<DE.Cell>
|
||||||
<DE.Label class="font-semibold">Tgl. Kunjungan</DE.Label>
|
<DE.Label position="dynamic" class="font-semibold">Tgl. Masuk</DE.Label>
|
||||||
|
<DE.Colon />
|
||||||
<DE.Field>
|
<DE.Field>
|
||||||
{{ data.visitDate.substring(0, 10) }}
|
{{ data.visitDate.substring(0, 10) }}
|
||||||
</DE.Field>
|
</DE.Field>
|
||||||
</DE.Cell>
|
</DE.Cell>
|
||||||
<DE.Cell>
|
<DE.Cell>
|
||||||
<DE.Label class="font-semibold">Klinik</DE.Label>
|
<DE.Label position="dynamic" class="font-semibold">Poliklinik</DE.Label>
|
||||||
|
<DE.Colon />
|
||||||
<DE.Field>
|
<DE.Field>
|
||||||
{{ data.unit?.name }}
|
{{ data.unit?.name }}
|
||||||
</DE.Field>
|
</DE.Field>
|
||||||
</DE.Cell>
|
</DE.Cell>
|
||||||
<DE.Cell>
|
<DE.Cell>
|
||||||
<DE.Label class="font-semibold">DPJP</DE.Label>
|
<DE.Label position="dynamic" class="font-semibold">Klinik</DE.Label>
|
||||||
|
<DE.Colon />
|
||||||
<DE.Field>
|
<DE.Field>
|
||||||
{{ dpjp }}
|
{{ data.unit?.name }}
|
||||||
</DE.Field>
|
</DE.Field>
|
||||||
</DE.Cell>
|
</DE.Cell>
|
||||||
</DE.Block>
|
</DE.Block>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<DE.Block
|
<DE.Block mode="preview">
|
||||||
mode="preview"
|
<DE.Cell>
|
||||||
labelSize="large"
|
<DE.Label position="dynamic" class="font-semibold">DPJP</DE.Label>
|
||||||
>
|
<DE.Colon />
|
||||||
|
<DE.Field>
|
||||||
|
{{ dpjp }}
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
<DE.Cell>
|
<DE.Cell>
|
||||||
<DE.Label
|
<DE.Label
|
||||||
position="dynamic"
|
position="dynamic"
|
||||||
@@ -90,6 +94,7 @@ if (props.data.responsible_doctor) {
|
|||||||
>
|
>
|
||||||
Billing
|
Billing
|
||||||
</DE.Label>
|
</DE.Label>
|
||||||
|
<DE.Colon class="pt-1" />
|
||||||
<DE.Field class="text-base 2xl:text-lg">
|
<DE.Field class="text-base 2xl:text-lg">
|
||||||
Rp. 000.000
|
Rp. 000.000
|
||||||
<!-- {{ data }} -->
|
<!-- {{ data }} -->
|
||||||
|
|||||||
@@ -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',
|
review: 'fresh',
|
||||||
process: 'fresh',
|
process: 'fresh',
|
||||||
done: 'positive',
|
done: 'positive',
|
||||||
canceled: 'destructive',
|
cancel: 'destructive',
|
||||||
rejected: 'destructive',
|
rejected: 'destructive',
|
||||||
skiped: 'negative',
|
skiped: 'negative',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,12 +143,16 @@ watch(props, (value) => {
|
|||||||
nationalId.value = objects?.nationalIdentity || '-'
|
nationalId.value = objects?.nationalIdentity || '-'
|
||||||
medicalRecordNumber.value = objects?.medicalRecordNumber || '-'
|
medicalRecordNumber.value = objects?.medicalRecordNumber || '-'
|
||||||
patientName.value = objects?.patientName || '-'
|
patientName.value = objects?.patientName || '-'
|
||||||
|
phoneNumber.value = objects?.phoneNumber || '-'
|
||||||
if (objects?.sepType === 'internal') {
|
if (objects?.sepType === 'internal') {
|
||||||
admissionType.value = '4'
|
admissionType.value = '4'
|
||||||
}
|
}
|
||||||
if (objects?.sepType === 'external') {
|
if (objects?.sepType === 'external') {
|
||||||
admissionType.value = '1'
|
admissionType.value = '1'
|
||||||
}
|
}
|
||||||
|
if (objects?.diagnoseLabel) {
|
||||||
|
initialDiagnosis.value = objects?.diagnoseLabel
|
||||||
|
}
|
||||||
isDateReload.value = true
|
isDateReload.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (objects?.letterDate) {
|
if (objects?.letterDate) {
|
||||||
@@ -176,6 +180,9 @@ onMounted(() => {
|
|||||||
if (!isService.value) {
|
if (!isService.value) {
|
||||||
serviceType.value = '2'
|
serviceType.value = '2'
|
||||||
}
|
}
|
||||||
|
if (!admissionType.value) {
|
||||||
|
admissionType.value = '1'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ const props = defineProps<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PubMyUiDataTable
|
<PubMyUiDataTable
|
||||||
v-bind="config"
|
|
||||||
:rows="props.data"
|
:rows="props.data"
|
||||||
|
v-bind="config"
|
||||||
|
v-on="$attrs"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { defineAsyncComponent } from 'vue'
|
|||||||
|
|
||||||
type SmallDetailDto = any
|
type SmallDetailDto = any
|
||||||
|
|
||||||
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
|
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
|
||||||
|
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
cols: [{}, {}, {}, { width: 100 }, { width: 120 }, {}, {}, {}, { width: 100 }, { width: 100 }, {}, { width: 50 }],
|
cols: [{}, {}, {}, { width: 100 }, { width: 120 }, {}, {}, {}, { width: 100 }, { width: 100 }, {}, { width: 50 }],
|
||||||
@@ -20,7 +20,7 @@ export const config: Config = {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
keys: ['time', 'employee_id', 'main_complaint', 'encounter_id', 'diagnose', 'status', 'action'],
|
keys: ['time', 'employee_id', 'main_complaint', 'encounter', 'diagnose', 'status', 'action'],
|
||||||
|
|
||||||
delKeyNames: [
|
delKeyNames: [
|
||||||
{ key: 'code', label: 'Kode' },
|
{ key: 'code', label: 'Kode' },
|
||||||
@@ -44,6 +44,10 @@ export const config: Config = {
|
|||||||
return '-'
|
return '-'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
encounter(rec: any) {
|
||||||
|
const data = rec?.encounter ?? {}
|
||||||
|
return data?.class_code || '-'
|
||||||
|
},
|
||||||
diagnose(rec: any) {
|
diagnose(rec: any) {
|
||||||
const { value } = rec ?? {}
|
const { value } = rec ?? {}
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import EncounterHome from '~/components/content/encounter/home.vue'
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<EncounterHome classes="chemotherapy" />
|
<EncounterHome display="menu" class-code="ambulatory" sub-class-code="chemo" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useQueryMode } from '@/composables/useQueryMode'
|
||||||
|
|
||||||
|
import List from './list.vue'
|
||||||
|
import Form from './form.vue'
|
||||||
|
|
||||||
|
// Models
|
||||||
|
import type { Encounter } from '~/models/encounter'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
interface Props {
|
||||||
|
encounter: Encounter
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const { mode, goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<List
|
||||||
|
v-if="mode === 'list'"
|
||||||
|
:encounter="props.encounter"
|
||||||
|
@add="goToEntry"
|
||||||
|
@edit="goToEntry"
|
||||||
|
/>
|
||||||
|
<Form
|
||||||
|
v-else
|
||||||
|
@back="backToList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { z } from 'zod'
|
||||||
|
import Entry from '~/components/app/cprj/entry.vue'
|
||||||
|
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||||
|
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import { CprjSoapiSchema } from '~/schemas/soapi.schema'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
||||||
|
const { backToList } = useQueryMode('mode')
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const fungsional = ref([])
|
||||||
|
const schema = CprjSoapiSchema
|
||||||
|
const payload = ref({
|
||||||
|
encounter_id: 0,
|
||||||
|
time: '',
|
||||||
|
typeCode: 'dev-record',
|
||||||
|
value: '',
|
||||||
|
})
|
||||||
|
const model = ref({
|
||||||
|
ppa: '',
|
||||||
|
ppa_name: '',
|
||||||
|
subjective: '',
|
||||||
|
objective: '',
|
||||||
|
assesment: '',
|
||||||
|
plan: '',
|
||||||
|
review: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLoading = reactive<DataTableLoader>({
|
||||||
|
isTableLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {})
|
||||||
|
|
||||||
|
const cprjRef = ref()
|
||||||
|
async function actionHandler(type: string) {
|
||||||
|
if (type === 'back') {
|
||||||
|
backToList()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = await cprjRef.value?.validate()
|
||||||
|
console.log('result', result)
|
||||||
|
if (result?.valid) {
|
||||||
|
console.log('data', result.data)
|
||||||
|
handleActionSave(
|
||||||
|
{
|
||||||
|
...payload.value,
|
||||||
|
value: JSON.stringify(result.data),
|
||||||
|
encounter_id: +route.params.id,
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
toast,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.log('Ada error di form', result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Entry
|
||||||
|
ref="cprjRef"
|
||||||
|
v-model="model"
|
||||||
|
:schema="schema"
|
||||||
|
type="function"
|
||||||
|
/>
|
||||||
|
<div class="my-2 flex justify-end py-2">
|
||||||
|
<Action @click="actionHandler" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||||
|
import List from '~/components/app/soapi/list.vue'
|
||||||
|
import Entry from '~/components/app/cprj/entry.vue'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
import {
|
||||||
|
recId,
|
||||||
|
recAction,
|
||||||
|
recItem,
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleActionEdit,
|
||||||
|
handleActionRemove,
|
||||||
|
handleCancelForm,
|
||||||
|
} from '~/handlers/consultation.handler'
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { getList, getDetail } from '~/services/soapi-early.service'
|
||||||
|
|
||||||
|
// Models
|
||||||
|
import type { Encounter } from '~/models/encounter'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
interface Props {
|
||||||
|
encounter: Encounter
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emits = defineEmits(['add', 'edit'])
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const { recordId } = useQueryCRUDRecordId()
|
||||||
|
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||||
|
|
||||||
|
let units = ref<{ value: string; label: string }[]>([])
|
||||||
|
const encounterId = ref<number>(props?.encounter?.id || 0)
|
||||||
|
const title = ref('')
|
||||||
|
const id = route.params.id
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
paginationMeta,
|
||||||
|
searchInput,
|
||||||
|
handlePageChange,
|
||||||
|
handleSearch,
|
||||||
|
fetchData: getMyList,
|
||||||
|
} = usePaginatedList({
|
||||||
|
fetchFn: async ({ page, search }) => {
|
||||||
|
const result = await getList({
|
||||||
|
'encounter-id': id,
|
||||||
|
typeCode: 'dev-record',
|
||||||
|
includes: 'encounter',
|
||||||
|
search,
|
||||||
|
page,
|
||||||
|
})
|
||||||
|
if (result.success) {
|
||||||
|
data.value = result.body.data
|
||||||
|
}
|
||||||
|
return { success: result.success || false, body: result.body || {} }
|
||||||
|
},
|
||||||
|
entityName: 'cprj',
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: 'CPRJ',
|
||||||
|
icon: 'i-lucide-box',
|
||||||
|
refSearchNav: {
|
||||||
|
placeholder: 'Cari (min. 3 karakter)...',
|
||||||
|
minLength: 3,
|
||||||
|
debounceMs: 500,
|
||||||
|
showValidationFeedback: true,
|
||||||
|
onInput: (value: string) => {
|
||||||
|
searchInput.value = value
|
||||||
|
},
|
||||||
|
onClick: () => {},
|
||||||
|
onClear: () => {},
|
||||||
|
},
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah',
|
||||||
|
icon: 'i-lucide-plus',
|
||||||
|
onClick: () => {
|
||||||
|
goToEntry()
|
||||||
|
emits('add')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
|
||||||
|
const getMyDetail = async (id: number | string) => {
|
||||||
|
const result = await getDetail(id)
|
||||||
|
if (result.success) {
|
||||||
|
const currentValue = result.body?.data || {}
|
||||||
|
recItem.value = currentValue
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for row actions when recId or recAction changes
|
||||||
|
watch([recId, recAction], () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showDetail:
|
||||||
|
getMyDetail(recId.value)
|
||||||
|
title.value = 'Detail Konsultasi'
|
||||||
|
isReadonly.value = true
|
||||||
|
break
|
||||||
|
case ActionEvents.showEdit:
|
||||||
|
emits('edit')
|
||||||
|
recordId.value = recId.value
|
||||||
|
console.log('recordId', recId.value)
|
||||||
|
break
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getMyList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header
|
||||||
|
v-model="searchInput"
|
||||||
|
:prep="headerPrep"
|
||||||
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
|
@search="handleSearch"
|
||||||
|
class="mb-4 xl:mb-5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<List
|
||||||
|
:data="data"
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Record Confirmation Modal -->
|
||||||
|
<RecordConfirmation
|
||||||
|
v-model:open="isRecordConfirmationOpen"
|
||||||
|
action="delete"
|
||||||
|
:record="recItem"
|
||||||
|
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||||
|
@cancel=""
|
||||||
|
>
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>ID:</strong>
|
||||||
|
{{ record?.id }}
|
||||||
|
</p>
|
||||||
|
<p v-if="record?.name">
|
||||||
|
<strong>Nama:</strong>
|
||||||
|
{{ record.name }}
|
||||||
|
</p>
|
||||||
|
<p v-if="record?.code">
|
||||||
|
<strong>Kode:</strong>
|
||||||
|
{{ record.code }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</template>
|
||||||
@@ -20,8 +20,8 @@ const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
|
|||||||
const { getPagePermissions } = useRBAC()
|
const { getPagePermissions } = useRBAC()
|
||||||
const pagePermission = getPagePermissions(roleAccess)
|
const pagePermission = getPagePermissions(roleAccess)
|
||||||
|
|
||||||
const {user,userRole} = useUserStore()
|
// const {user,userRole} = useUserStore()
|
||||||
const {getUserPermissions} = useRBAC()
|
// const {getUserPermissions} = useRBAC()
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region State
|
// #region State
|
||||||
@@ -121,6 +121,7 @@ watch([recId, recAction, timestamp], () => {
|
|||||||
case ActionEvents.showDetail:
|
case ActionEvents.showDetail:
|
||||||
isDocPreviewDialogOpen.value = true
|
isDocPreviewDialogOpen.value = true
|
||||||
break
|
break
|
||||||
|
|
||||||
case ActionEvents.showEdit:
|
case ActionEvents.showEdit:
|
||||||
if(pagePermission.canUpdate){
|
if(pagePermission.canUpdate){
|
||||||
navigateTo({
|
navigateTo({
|
||||||
@@ -131,22 +132,13 @@ watch([recId, recAction, timestamp], () => {
|
|||||||
unauthorizedToast()
|
unauthorizedToast()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case ActionEvents.showConfirmDelete:
|
case ActionEvents.showConfirmDelete:
|
||||||
if(pagePermission.canDelete){
|
if(pagePermission.canDelete){
|
||||||
isRecordConfirmationOpen.value = true
|
isRecordConfirmationOpen.value = true
|
||||||
} else {
|
} else {
|
||||||
unauthorizedToast()
|
unauthorizedToast()
|
||||||
}
|
}
|
||||||
navigateTo(recItem.value.filePath, { external: true, open: { target: '_blank' } })
|
|
||||||
break
|
|
||||||
case ActionEvents.showEdit:
|
|
||||||
navigateTo({
|
|
||||||
name: 'rehab-encounter-id-document-upload-document_id-edit',
|
|
||||||
params: { id: encounterId, "document_id": recId.value },
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case ActionEvents.showConfirmDelete:
|
|
||||||
isRecordConfirmationOpen.value = true
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,42 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Components
|
// Components
|
||||||
import { toast } from '~/components/pub/ui/toast'
|
|
||||||
import { Button } from '~/components/pub/ui/button'
|
import { Button } from '~/components/pub/ui/button'
|
||||||
import AppEncounterEntryForm from '~/components/app/encounter/entry-form.vue'
|
import AppEncounterEntryForm from '~/components/app/encounter/entry-form.vue'
|
||||||
import AppViewPatient from '~/components/app/patient/view-patient.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
|
// Helpers
|
||||||
import { refDebounced } from '@vueuse/core'
|
import { refDebounced } from '@vueuse/core'
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
import {
|
import { useEncounterEntry } from '~/handlers/encounter-entry.handler'
|
||||||
patients,
|
|
||||||
selectedPatient,
|
|
||||||
selectedPatientObject,
|
|
||||||
paginationMeta,
|
|
||||||
getPatientsList,
|
|
||||||
getPatientCurrent,
|
|
||||||
getPatientByIdentifierSearch,
|
|
||||||
} from '~/handlers/patient.handler'
|
|
||||||
|
|
||||||
// Stores
|
|
||||||
import { useUserStore } from '~/stores/user'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
id: number
|
id: number
|
||||||
@@ -46,54 +18,43 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const route = useRoute()
|
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 formRef = ref<InstanceType<typeof AppEncounterEntryForm> | null>(null)
|
||||||
const encounterData = ref<any>(null)
|
|
||||||
const formObjects = ref<any>({})
|
|
||||||
|
|
||||||
// SEP validation state
|
const {
|
||||||
const isSepValid = ref(false)
|
paymentsList,
|
||||||
const isCheckingSep = ref(false)
|
sepNumber,
|
||||||
const sepNumber = ref('')
|
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)
|
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() {
|
function handleSavePatient() {
|
||||||
selectedPatientObject.value = null
|
selectedPatientObject.value = null
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -101,140 +62,15 @@ function handleSavePatient() {
|
|||||||
}, 150)
|
}, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toKebabCase(str: string): string {
|
function handleSaveClick() {
|
||||||
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
|
if (formRef.value && typeof formRef.value.submitForm === 'function') {
|
||||||
|
formRef.value.submitForm()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toNavigateSep(values: any) {
|
function handleFetch(value?: any) {
|
||||||
const queryParams = new URLSearchParams()
|
if (value?.subSpecialistId) {
|
||||||
if (values['subSpecialistCode']) {
|
handleFetchDoctors(value.subSpecialistId)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,11 +82,9 @@ async function handleEvent(menu: string, value?: any) {
|
|||||||
} else if (menu === 'add') {
|
} else if (menu === 'add') {
|
||||||
navigateTo('/client/patient/add')
|
navigateTo('/client/patient/add')
|
||||||
} else if (menu === 'add-sep') {
|
} else if (menu === 'add-sep') {
|
||||||
// If SEP is already valid, don't navigate
|
|
||||||
if (isSepValid.value) {
|
if (isSepValid.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
recSelectId.value = null
|
|
||||||
toNavigateSep({
|
toNavigateSep({
|
||||||
isService: 'false',
|
isService: 'false',
|
||||||
sourcePath: route.path,
|
sourcePath: route.path,
|
||||||
@@ -258,10 +92,7 @@ async function handleEvent(menu: string, value?: any) {
|
|||||||
...value,
|
...value,
|
||||||
})
|
})
|
||||||
} else if (menu === 'sep-number-changed') {
|
} else if (menu === 'sep-number-changed') {
|
||||||
// Update sepNumber when it changes in form (only if different to prevent loop)
|
await getValidateSepNumber(String(value || ''))
|
||||||
if (sepNumber.value !== value) {
|
|
||||||
sepNumber.value = value || ''
|
|
||||||
}
|
|
||||||
} else if (menu === 'save') {
|
} else if (menu === 'save') {
|
||||||
await handleSaveEncounter(value)
|
await handleSaveEncounter(value)
|
||||||
} else if (menu === 'cancel') {
|
} else if (menu === 'cancel') {
|
||||||
@@ -269,432 +100,26 @@ async function handleEvent(menu: string, value?: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle save button click
|
provide('rec_select_id', recSelectId)
|
||||||
function handleSaveClick() {
|
provide('table_data_loader', isLoading)
|
||||||
console.log('🔵 handleSaveClick called')
|
|
||||||
console.log('🔵 formRef:', formRef.value)
|
|
||||||
console.log('🔵 isSaveDisabled:', isSaveDisabled.value)
|
|
||||||
console.log('🔵 selectedPatient:', selectedPatient.value)
|
|
||||||
console.log('🔵 selectedPatientObject:', selectedPatientObject.value)
|
|
||||||
console.log('🔵 isSaving:', isSaving.value)
|
|
||||||
console.log('🔵 isLoadingDetail:', isLoadingDetail.value)
|
|
||||||
|
|
||||||
if (formRef.value && typeof formRef.value.submitForm === 'function') {
|
|
||||||
console.log('🔵 Calling formRef.value.submitForm()')
|
|
||||||
formRef.value.submitForm()
|
|
||||||
} else {
|
|
||||||
console.error('❌ formRef.value is null or submitForm is not a function')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate SEP number
|
|
||||||
*/
|
|
||||||
async function validateSepNumber(sepNumberValue: string) {
|
|
||||||
// Reset validation if SEP number is empty
|
|
||||||
if (!sepNumberValue || sepNumberValue.trim() === '') {
|
|
||||||
isSepValid.value = false
|
|
||||||
isCheckingSep.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only check if payment type is JKN
|
|
||||||
// We need to check from formObjects
|
|
||||||
const paymentType = formObjects.value?.paymentType
|
|
||||||
if (paymentType !== 'jkn') {
|
|
||||||
isSepValid.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
isCheckingSep.value = true
|
|
||||||
const result = await getSepList({ number: sepNumberValue.trim() })
|
|
||||||
|
|
||||||
// Check if SEP is found
|
|
||||||
// If response is not null, SEP is found
|
|
||||||
// If response is null with metaData code "201", SEP is not found
|
|
||||||
if (result.success && result.body?.response !== null) {
|
|
||||||
isSepValid.value = true
|
|
||||||
} else {
|
|
||||||
// SEP not found (response null with metaData code "201")
|
|
||||||
isSepValid.value = false
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking SEP:', error)
|
|
||||||
isSepValid.value = false
|
|
||||||
} finally {
|
|
||||||
isCheckingSep.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch debounced SEP number to validate
|
|
||||||
watch(debouncedSepNumber, async (newValue) => {
|
watch(debouncedSepNumber, async (newValue) => {
|
||||||
await validateSepNumber(newValue)
|
await getValidateSepNumber(newValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch payment type to reset SEP validation
|
|
||||||
watch(
|
watch(
|
||||||
() => formObjects.value?.paymentType,
|
() => formObjects.value?.paymentType,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
isSepValid.value = false
|
isSepValid.value = false
|
||||||
// If payment type is not JKN, clear SEP number
|
|
||||||
if (newValue !== 'jkn') {
|
if (newValue !== 'jkn') {
|
||||||
sepNumber.value = ''
|
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 () => {
|
onMounted(async () => {
|
||||||
await handleInit()
|
await handleInit()
|
||||||
// Load encounter detail if in edit mode
|
if (props.id > 0) {
|
||||||
if (isEditMode.value) {
|
|
||||||
await loadEncounterDetail()
|
await loadEncounterDetail()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -1,30 +1,40 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Components
|
///// Imports
|
||||||
import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
|
// Pub components
|
||||||
import SummaryCard from '~/components/pub/my-ui/summary-card/summary-card.vue'
|
|
||||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
|
||||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
|
||||||
import Filter from '~/components/pub/my-ui/nav-header/filter.vue'
|
|
||||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
|
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
|
||||||
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
|
||||||
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import * as CH from '~/components/pub/my-ui/content-header'
|
||||||
|
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||||
|
import { useSidebar } from '~/components/pub/ui/sidebar/utils'
|
||||||
|
|
||||||
|
// App libs
|
||||||
|
import { getServicePosition } from '~/lib/roles' // previously getPositionAs
|
||||||
|
|
||||||
// Services
|
// 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
|
// Apps
|
||||||
import { toast } from '~/components/pub/ui/toast'
|
import Content from '~/components/app/encounter/list.vue'
|
||||||
|
import FilterNav from '~/components/app/encounter/filter-nav.vue'
|
||||||
|
import FilterForm from '~/components/app/encounter/filter-form.vue'
|
||||||
|
|
||||||
|
// Props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient'
|
classCode?: 'ambulatory' | 'emergency' | 'inpatient'
|
||||||
subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
|
subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
|
||||||
type: string
|
canCreate?: boolean
|
||||||
|
canUpdate?: boolean
|
||||||
|
canDelete?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
///// Declarations and Flows
|
||||||
|
// Sidebar automation
|
||||||
|
const { setOpen } = useSidebar()
|
||||||
|
setOpen(true)
|
||||||
|
|
||||||
|
// Main data
|
||||||
const data = ref([])
|
const data = ref([])
|
||||||
const isLoading = reactive<DataTableLoader>({
|
const isLoading = reactive<DataTableLoader>({
|
||||||
summary: false,
|
summary: false,
|
||||||
@@ -33,66 +43,88 @@ const isLoading = reactive<DataTableLoader>({
|
|||||||
const recId = ref<number>(0)
|
const recId = ref<number>(0)
|
||||||
const recAction = ref<string>('')
|
const recAction = ref<string>('')
|
||||||
const recItem = ref<any>(null)
|
const recItem = ref<any>(null)
|
||||||
const isFormEntryDialogOpen = ref(false)
|
const isFilterFormDialogOpen = ref(false)
|
||||||
const isRecordConfirmationOpen = ref(false)
|
const isRecordConfirmationOpen = ref(false)
|
||||||
|
const isRecordCancelOpen = ref(false)
|
||||||
|
|
||||||
const hreaderPrep: HeaderPrep = {
|
// Headers
|
||||||
|
const hreaderPrep: CH.Config = {
|
||||||
title: 'Kunjungan',
|
title: 'Kunjungan',
|
||||||
icon: 'i-lucide-users',
|
icon: 'i-lucide-users',
|
||||||
addNav: {
|
addNav: {
|
||||||
label: 'Tambah',
|
label: 'Tambah',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (props.classCode === 'ambulatory' && props.subClassCode === 'rehab') {
|
navigateTo(`/${props.classCode}/encounter/add`)
|
||||||
navigateTo('/rehab/encounter/add')
|
|
||||||
}
|
|
||||||
if (props.classCode === 'ambulatory' && props.subClassCode === 'reg') {
|
|
||||||
navigateTo('/outpatient/encounter/add')
|
|
||||||
}
|
|
||||||
if (props.classCode === 'emergency') {
|
|
||||||
navigateTo('/emergency/encounter/add')
|
|
||||||
}
|
|
||||||
if (props.classCode === 'inpatient') {
|
|
||||||
navigateTo('/inpatient/encounter/add')
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if (!props.canCreate) {
|
||||||
const refSearchNav: RefSearchNav = {
|
delete hreaderPrep.addNav
|
||||||
onClick: () => {
|
|
||||||
// open filter modal
|
|
||||||
isFormEntryDialogOpen.value = true
|
|
||||||
console.log(' 1open filter modal')
|
|
||||||
},
|
|
||||||
onInput: (_val: string) => {
|
|
||||||
// filter patient list
|
|
||||||
},
|
|
||||||
onClear: () => {
|
|
||||||
// clear url param
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading state management
|
// Filters
|
||||||
|
const filter = ref<{
|
||||||
|
installation: {
|
||||||
|
msg: {
|
||||||
|
placeholder: string
|
||||||
|
}
|
||||||
|
items: {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
code: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
schema: any
|
||||||
|
initialValues?: Partial<any>
|
||||||
|
errors?: any
|
||||||
|
}>({
|
||||||
|
installation: {
|
||||||
|
msg: {
|
||||||
|
placeholder: 'Pilih',
|
||||||
|
},
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
schema: {},
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
// Role reactivities
|
||||||
* Get base path for encounter routes based on classCode and subClassCode
|
const { getActiveRole } = useUserStore()
|
||||||
*/
|
const activeServicePosition = ref(getServicePosition(getActiveRole()))
|
||||||
function getBasePath(): string {
|
provide('activeServicePosition', activeServicePosition)
|
||||||
if (props.classCode === 'ambulatory' && props.subClassCode === 'rehab') {
|
watch(getActiveRole, (role? : string) => {
|
||||||
return '/rehab/encounter'
|
activeServicePosition.value = getServicePosition(role)
|
||||||
}
|
})
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Recrod reactivities
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
watch(() => recAction.value, () => {
|
||||||
|
const basePath = `/${props.classCode}/encounter`
|
||||||
|
// console.log(`${basePath}/${recId.value}`, recAction.value)
|
||||||
|
// return
|
||||||
|
if (recAction.value === ActionEvents.showConfirmDelete) {
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
} else if (recAction.value === ActionEvents.showCancel) {
|
||||||
|
isRecordCancelOpen.value = true
|
||||||
|
} else if (recAction.value === ActionEvents.showDetail) {
|
||||||
|
navigateTo(`${basePath}/${recId.value}`)
|
||||||
|
} else if (recAction.value === ActionEvents.showEdit) {
|
||||||
|
navigateTo(`${basePath}/${recId.value}/edit`)
|
||||||
|
} else if (recAction.value === ActionEvents.showProcess) {
|
||||||
|
navigateTo(`${basePath}/${recId.value}/process`)
|
||||||
|
} else if (recAction.value === ActionEvents.showConfirmDelete) {
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
}
|
||||||
|
recAction.value = '' // reset
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getPatientList()
|
||||||
|
})
|
||||||
|
|
||||||
|
/////// Functions
|
||||||
async function getPatientList() {
|
async function getPatientList() {
|
||||||
isLoading.isTableLoading = true
|
isLoading.isTableLoading = true
|
||||||
try {
|
try {
|
||||||
@@ -114,6 +146,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
|
// Handle confirmation result
|
||||||
async function handleConfirmDelete(record: any, action: string) {
|
async function handleConfirmDelete(record: any, action: string) {
|
||||||
if (action === 'delete' && record?.id) {
|
if (action === 'delete' && record?.id) {
|
||||||
@@ -156,90 +225,72 @@ function handleCancelConfirmation() {
|
|||||||
recId.value = 0
|
recId.value = 0
|
||||||
recAction.value = ''
|
recAction.value = ''
|
||||||
recItem.value = null
|
recItem.value = null
|
||||||
isRecordConfirmationOpen.value = false
|
isRecordCancelOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
function handleRemoveConfirmation() {
|
||||||
() => recAction.value,
|
// Reset record state when cancelled
|
||||||
() => {
|
recId.value = 0
|
||||||
if (recAction.value === ActionEvents.showConfirmDelete) {
|
recAction.value = ''
|
||||||
isRecordConfirmationOpen.value = true
|
recItem.value = null
|
||||||
return
|
isRecordConfirmationOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePath = getBasePath()
|
|
||||||
|
|
||||||
if (props.type === 'encounter') {
|
|
||||||
if (recAction.value === 'showDetail') {
|
|
||||||
navigateTo(`${basePath}/${recId.value}/detail`)
|
|
||||||
} else if (recAction.value === 'showEdit') {
|
|
||||||
navigateTo(`${basePath}/${recId.value}/edit`)
|
|
||||||
} else if (recAction.value === 'showProcess') {
|
|
||||||
navigateTo(`${basePath}/${recId.value}/process`)
|
|
||||||
} else {
|
|
||||||
// handle other actions
|
|
||||||
}
|
|
||||||
} else if (props.type === 'registration') {
|
|
||||||
// Handle registration type if needed
|
|
||||||
if (recAction.value === 'showDetail') {
|
|
||||||
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/detail`)
|
|
||||||
} else if (recAction.value === 'showEdit') {
|
|
||||||
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/edit`)
|
|
||||||
} else if (recAction.value === 'showProcess') {
|
|
||||||
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/process`)
|
|
||||||
} else {
|
|
||||||
// handle other actions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
provide('rec_id', recId)
|
|
||||||
provide('rec_action', recAction)
|
|
||||||
provide('rec_item', recItem)
|
|
||||||
provide('table_data_loader', isLoading)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getPatientList()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Header
|
<CH.ContentHeader v-bind="hreaderPrep">
|
||||||
:prep="{ ...hreaderPrep }"
|
<FilterNav
|
||||||
:ref-search-nav="refSearchNav"
|
@onFilterClick="() => isFilterFormDialogOpen = true"
|
||||||
/>
|
@onExportPdf="() => {}"
|
||||||
<Separator class="my-4 xl:my-5" />
|
@onExportExcel="() => {}"
|
||||||
|
@nExportCsv="() => {}"
|
||||||
|
/>
|
||||||
|
</CH.ContentHeader>
|
||||||
|
|
||||||
<Filter
|
<Content :data="data" />
|
||||||
:prep="hreaderPrep"
|
|
||||||
:ref-search-nav="refSearchNav"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AppEncounterList :data="data" />
|
|
||||||
|
|
||||||
|
<!-- Filter -->
|
||||||
<Dialog
|
<Dialog
|
||||||
v-model:open="isFormEntryDialogOpen"
|
v-model:open="isFilterFormDialogOpen"
|
||||||
title="Filter"
|
title="Filter"
|
||||||
size="lg"
|
size="lg"
|
||||||
prevent-outside
|
prevent-outside
|
||||||
>
|
>
|
||||||
<AppEncounterFilter
|
<FilterForm v-bind="filter" />
|
||||||
:installation="{
|
|
||||||
msg: { placeholder: 'Pilih' },
|
|
||||||
items: [],
|
|
||||||
}"
|
|
||||||
:schema="{}"
|
|
||||||
/>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- Record Confirmation Modal -->
|
<!-- Batal -->
|
||||||
<RecordConfirmation
|
<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>
|
||||||
|
|
||||||
|
<!-- Hapus -->
|
||||||
|
<RecordConfirmation
|
||||||
|
v-if="canDelete"
|
||||||
v-model:open="isRecordConfirmationOpen"
|
v-model:open="isRecordConfirmationOpen"
|
||||||
action="delete"
|
action="delete"
|
||||||
:record="recItem"
|
:record="recItem"
|
||||||
@confirm="handleConfirmDelete"
|
@confirm="handleConfirmDelete"
|
||||||
@cancel="handleCancelConfirmation"
|
@cancel="handleRemoveConfirmation"
|
||||||
>
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
@@ -258,4 +309,11 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</RecordConfirmation>
|
</RecordConfirmation>
|
||||||
|
<Dialog
|
||||||
|
title="Hapus data"
|
||||||
|
size="md"
|
||||||
|
v-model:open="isRecordConfirmationOpen"
|
||||||
|
>
|
||||||
|
Hak akses tidak memenuhi kriteria untuk proses ini.
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<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'
|
||||||
|
|
||||||
|
import { genEncounter } from '~/models/encounter'
|
||||||
|
|
||||||
|
// PLASE ORDER BY TAB POSITION
|
||||||
|
import Status from '~/components/content/encounter/status.vue'
|
||||||
|
import AssesmentFunctionList from '~/components/content/soapi/entry.vue'
|
||||||
|
import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
|
||||||
|
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
|
||||||
|
import DeviceOrder from '~/components/content/device-order/main.vue'
|
||||||
|
import Prescription from '~/components/content/prescription/main.vue'
|
||||||
|
import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
|
||||||
|
import Radiology from '~/components/content/radiology-order/main.vue'
|
||||||
|
import Consultation from '~/components/content/consultation/list.vue'
|
||||||
|
import Cprj from '~/components/content/cprj/entry.vue'
|
||||||
|
import DocUploadList from '~/components/content/document-upload/list.vue'
|
||||||
|
import GeneralConsentList from '~/components/content/general-consent/entry.vue'
|
||||||
|
import ResumeList from '~/components/content/resume/list.vue'
|
||||||
|
import ControlLetterList from '~/components/content/control-letter/list.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 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 data = ref(genEncounter())
|
||||||
|
|
||||||
|
async function fetchDetail() {
|
||||||
|
const res = await getDetail(id, {
|
||||||
|
includes:
|
||||||
|
'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person,EncounterDocuments',
|
||||||
|
})
|
||||||
|
if (res.body?.data) data.value = res.body?.data
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDetail()
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs: TabItem[] = [
|
||||||
|
{ value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } },
|
||||||
|
{
|
||||||
|
value: 'early-medical-assessment',
|
||||||
|
label: 'Pengkajian Awal Medis',
|
||||||
|
component: EarlyMedicalAssesmentList,
|
||||||
|
props: { encounter: data, type: 'early-medic', label: 'Pengkajian Awal Medis' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'rehab-medical-assessment',
|
||||||
|
label: 'Pengkajian Awal Medis Rehabilitasi Medis',
|
||||||
|
component: EarlyMedicalRehabList,
|
||||||
|
props: { encounter: data, type: 'early-rehab', label: 'Pengkajian Awal Medis Rehabilitasi Medis' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'function-assessment',
|
||||||
|
label: 'Asesmen Fungsi',
|
||||||
|
component: AssesmentFunctionList,
|
||||||
|
props: { encounter: data, type: 'function', label: 'Asesmen Fungsi' },
|
||||||
|
},
|
||||||
|
{ value: 'therapy-protocol', label: 'Protokol Terapi' },
|
||||||
|
{ value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' },
|
||||||
|
{ value: 'patient-note', label: 'CPRJ', component: Cprj, props: { encounter: data } },
|
||||||
|
{ value: 'consent', label: 'General Consent', component: GeneralConsentList, props: { encounter: data } },
|
||||||
|
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.value.id } },
|
||||||
|
{ value: 'device-order', label: 'Order Alkes', component: DeviceOrder, props: { encounter_id: data.value.id } },
|
||||||
|
{ value: 'device', label: 'Order Alkes' },
|
||||||
|
{ value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.id } },
|
||||||
|
{ value: 'mcu-lab-cp', label: 'Order Lab PK', component: CpLabOrder, props: { encounter_id: data.id } },
|
||||||
|
{ value: 'mcu-lab-micro', label: 'Order Lab Mikro' },
|
||||||
|
{ value: 'mcu-lab-pa', label: 'Order Lab PA' },
|
||||||
|
{ value: 'medical-action', label: 'Order Ruang Tindakan' },
|
||||||
|
{ value: 'mcu-result', label: 'Hasil Penunjang' },
|
||||||
|
{ value: 'consultation', label: 'Konsultasi', component: Consultation, props: { encounter: data } },
|
||||||
|
{ value: 'resume', label: 'Resume', component: ResumeList, props: { encounter: data } },
|
||||||
|
{ value: 'control', label: 'Surat Kontrol', component: ControlLetterList, props: { encounter: data } },
|
||||||
|
{ value: 'screening', label: 'Skrinning MPP' },
|
||||||
|
{
|
||||||
|
value: 'supporting-document',
|
||||||
|
label: 'Upload Dokumen Pendukung',
|
||||||
|
component: DocUploadList,
|
||||||
|
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>
|
||||||
@@ -1,110 +1,142 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
//
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
///// Imports
|
||||||
|
// Pubs components
|
||||||
|
import ContentSwitcher from '~/components/pub/my-ui/content-switcher/content-switcher.vue'
|
||||||
|
import { useSidebar } from '~/components/pub/ui/sidebar/utils'
|
||||||
|
import SubMenu from '~/components/pub/my-ui/menus/submenu.vue'
|
||||||
|
import ContentNavBa from '~/components/pub/my-ui/nav-content/ba.vue'
|
||||||
|
|
||||||
import { getDetail } from '~/services/encounter.service'
|
// App libs
|
||||||
|
import { getServicePosition } from '~/lib/roles' // previously getPositionAs
|
||||||
|
|
||||||
//
|
// App Models
|
||||||
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
|
import { genEncounter, type Encounter } from '~/models/encounter'
|
||||||
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
|
|
||||||
|
|
||||||
import { genEncounter } from '~/models/encounter'
|
// Handlers
|
||||||
|
import type { EncounterProps } from '~/handlers/encounter-init.handler'
|
||||||
|
import { getEncounterData } from '~/handlers/encounter-process.handler'
|
||||||
|
import { getMenuItems } from "~/handlers/encounter-init.handler"
|
||||||
|
|
||||||
// PLASE ORDER BY TAB POSITION
|
// App Components
|
||||||
import Status from '~/components/content/encounter/status.vue'
|
import EncounterPatientInfo from '~/components/app/encounter/quick-info.vue'
|
||||||
import AssesmentFunctionList from '~/components/content/soapi/entry.vue'
|
import EncounterHistoryButtonMenu from '~/components/app/encounter/quick-shortcut.vue'
|
||||||
import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
|
|
||||||
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
|
|
||||||
import KfrList from '~/components/content/kfr/list.vue'
|
|
||||||
import TherapyProtocolList from '~/components/content/therapy-protocol/list.vue'
|
|
||||||
import DeviceOrder from '~/components/content/device-order/main.vue'
|
|
||||||
import Prescription from '~/components/content/prescription/main.vue'
|
|
||||||
import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
|
|
||||||
import Radiology from '~/components/content/radiology-order/main.vue'
|
|
||||||
import Consultation from '~/components/content/consultation/list.vue'
|
|
||||||
import DocUploadList from '~/components/content/document-upload/list.vue'
|
|
||||||
import GeneralConsentList from '~/components/content/general-consent/entry.vue'
|
|
||||||
import ResumeList from '~/components/content/resume/list.vue'
|
|
||||||
import ControlLetterList from '~/components/content/control-letter/list.vue'
|
|
||||||
|
|
||||||
|
///// Declarations and Flows
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
classCode: EncounterProps['classCode']
|
||||||
|
subClassCode?: EncounterProps['subClassCode']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Common preparations
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// activeTab selalu sinkron dengan query param
|
const { user, userActiveRole, getActiveRole } = useUserStore()
|
||||||
const activeTab = computed({
|
const activeRole = getActiveRole()
|
||||||
get: () => (route.query?.tab && typeof route.query.tab === 'string' ? route.query.tab : 'status'),
|
const activePosition = ref(getServicePosition(activeRole))
|
||||||
set: (val: string) => {
|
const menus = ref([] as any)
|
||||||
router.replace({ path: route.path, query: { tab: val } })
|
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 id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||||
const data = ref(genEncounter())
|
const data = ref<Encounter>(genEncounter())
|
||||||
|
const isShowPatient = computed(() => data.value && data.value?.patient?.person)
|
||||||
|
|
||||||
async function fetchDetail() {
|
const { setOpen } = useSidebar()
|
||||||
const res = await getDetail(id, {
|
setOpen(false)
|
||||||
includes: 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person,EncounterDocuments',
|
|
||||||
})
|
if (activePosition.value === 'none') { // if user position is none, redirect to home page
|
||||||
if(res.body?.data) data.value = res.body?.data
|
router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// Dummy rows for ProtocolList (matches keys expected by list-cfg.protocol)
|
||||||
fetchDetail()
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reacrtivities
|
||||||
|
watch(getActiveRole, () => {
|
||||||
|
activePosition.value = getServicePosition(userActiveRole)
|
||||||
|
initMenus()
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabs: TabItem[] = [
|
onMounted(async () => {
|
||||||
{ value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } },
|
await getData()
|
||||||
{
|
initMenus()
|
||||||
value: 'early-medical-assessment',
|
})
|
||||||
label: 'Pengkajian Awal Medis',
|
|
||||||
component: EarlyMedicalAssesmentList,
|
///// Functions
|
||||||
props: { encounter: data, type: 'early-medic', label: 'Pengkajian Awal Medis' },
|
function handleClick(type: string) {
|
||||||
},
|
if (type === 'draft') {
|
||||||
{
|
router.back()
|
||||||
value: 'rehab-medical-assessment',
|
}
|
||||||
label: 'Pengkajian Awal Medis Rehabilitasi Medis',
|
}
|
||||||
component: EarlyMedicalRehabList,
|
|
||||||
props: { encounter: data, type: 'early-rehab', label: 'Pengkajian Awal Medis Rehabilitasi Medis' },
|
function initMenus() {
|
||||||
},
|
menus.value = getMenuItems(id, props, user, {
|
||||||
{
|
encounter: data.value
|
||||||
value: 'kfr',
|
} as any, {
|
||||||
label: 'Formulir Rawat Jalan KFR',
|
protocolTheraphy: paginationMeta,
|
||||||
component: KfrList,
|
protocolChemotherapy: paginationMeta,
|
||||||
props: { encounter: data, type: 'kfr', label: 'Formulir Rawat Jalan KFR' },
|
medicineProtocolChemotherapy: paginationMeta,
|
||||||
},
|
})
|
||||||
{
|
}
|
||||||
value: 'function-assessment',
|
|
||||||
label: 'Asesmen Fungsi',
|
async function getData() {
|
||||||
component: AssesmentFunctionList,
|
data.value = await getEncounterData(id)
|
||||||
props: { encounter: data, type: 'function', label: 'Asesmen Fungsi' },
|
}
|
||||||
},
|
|
||||||
{ value: 'therapy-protocol', label: 'Protokol Terapi', component: TherapyProtocolList, props: { encounter: data } },
|
|
||||||
{ value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' },
|
|
||||||
{ value: 'consent', label: 'General Consent', component: GeneralConsentList, props: { encounter: data } },
|
|
||||||
{ value: 'patient-note', label: 'CPRJ' },
|
|
||||||
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.value.id } },
|
|
||||||
{ value: 'device-order', label: 'Order Alkes', component: DeviceOrder, props: { encounter_id: data.value.id } },
|
|
||||||
{ value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.value.id } },
|
|
||||||
{ value: 'mcu-lab-cp', label: 'Order Lab PK', component: CpLabOrder, props: { encounter_id: data.value.id } },
|
|
||||||
{ value: 'mcu-lab-micro', label: 'Order Lab Mikro' },
|
|
||||||
{ value: 'mcu-lab-pa', label: 'Order Lab PA' },
|
|
||||||
{ value: 'medical-action', label: 'Order Ruang Tindakan' },
|
|
||||||
{ value: 'mcu-result', label: 'Hasil Penunjang' },
|
|
||||||
{ value: 'consultation', label: 'Konsultasi', component: Consultation, props: { encounter: data } },
|
|
||||||
{ value: 'resume', label: 'Resume', component: ResumeList, props: { encounter: data } },
|
|
||||||
{ value: 'control', label: 'Surat Kontrol', component: ControlLetterList, props: { encounter: data } },
|
|
||||||
{ value: 'screening', label: 'Skrinning MPP' },
|
|
||||||
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung', component: DocUploadList, props: { encounter: data }, },
|
|
||||||
]
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="mb-4">
|
<div class="bg-white dark:bg-slate-800 p-4 2xl:p-5">
|
||||||
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" />
|
<div class="mb-4 flex">
|
||||||
|
<div>
|
||||||
|
<ContentNavBa label="Kembali" @click="handleClick" />
|
||||||
|
</div>
|
||||||
|
<!-- <div class="ms-auto pe-3 pt-1 text-end text-xl 2xl:text-2xl font-semibold">
|
||||||
|
Pasien: {{ data.patient.person.name }} --- No. RM: {{ data.patient.number }}
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<ContentSwitcher :active="1" :height="150">
|
||||||
|
<template v-slot:content1>
|
||||||
|
<EncounterPatientInfo v-if="isShowPatient" :data="data" />
|
||||||
|
</template>
|
||||||
|
<template v-slot:content2>
|
||||||
|
<EncounterHistoryButtonMenu v-if="isShowPatient" />
|
||||||
|
</template>
|
||||||
|
</ContentSwitcher>
|
||||||
</div>
|
</div>
|
||||||
<AppEncounterQuickInfo :data="data" />
|
<SubMenu :data="menus" :initial-active-menu="activeMenu" @change-menu="activeMenu = $event" />
|
||||||
<CompTab :data="tabs" :initial-active-tab="activeTab" @change-tab="activeTab = $event" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,483 +1,59 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import AppSepEntryForm from '~/components/app/sep/entry-form.vue'
|
import AppSepEntryForm from '~/components/app/sep/entry-form.vue'
|
||||||
import AppViewPatient from '~/components/app/patient/view-patient.vue'
|
import AppViewPatient from '~/components/app/patient/view-patient.vue'
|
||||||
import AppViewHistory from '~/components/app/sep/view-history.vue'
|
import AppViewHistory from '~/components/app/sep/view-history.vue'
|
||||||
import AppViewLetter from '~/components/app/sep/view-letter.vue'
|
import AppViewLetter from '~/components/app/sep/view-letter.vue'
|
||||||
import { toast } from '~/components/pub/ui/toast'
|
|
||||||
|
|
||||||
// Types
|
// Handler
|
||||||
import type { SepHistoryData } from '~/components/app/sep/list-cfg.history'
|
import useIntegrationSepEntry from '~/handlers/integration-sep-entry.handler'
|
||||||
import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
|
|
||||||
|
|
||||||
// Constants
|
const {
|
||||||
import {
|
histories,
|
||||||
serviceTypes,
|
letters,
|
||||||
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,
|
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,
|
selectedPatient,
|
||||||
selectedPatientObject,
|
|
||||||
paginationMeta,
|
paginationMeta,
|
||||||
|
getLetterMappers,
|
||||||
getPatientsList,
|
getPatientsList,
|
||||||
getPatientCurrent,
|
|
||||||
getPatientByIdentifierSearch,
|
getPatientByIdentifierSearch,
|
||||||
} from '~/handlers/patient.handler'
|
handleSaveLetter,
|
||||||
|
handleSavePatient,
|
||||||
const route = useRoute()
|
handleEvent,
|
||||||
const openPatient = ref(false)
|
handleFetch,
|
||||||
const openLetter = ref(false)
|
handleInit,
|
||||||
const openHistory = ref(false)
|
} = useIntegrationSepEntry()
|
||||||
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']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await handleInit()
|
await handleInit()
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Components
|
// 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 Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -11,141 +8,48 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
} from '~/components/pub/ui/dropdown-menu'
|
} from '~/components/pub/ui/dropdown-menu'
|
||||||
import AppSepList from '~/components/app/sep/list.vue'
|
import AppSepList from '~/components/app/sep/list.vue'
|
||||||
|
import RangeCalendar from '~/components/pub/ui/range-calendar/RangeCalendar.vue'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
import { X, Check } from 'lucide-vue-next'
|
import { X, Check } from 'lucide-vue-next'
|
||||||
|
|
||||||
// Types
|
// Libraries
|
||||||
import type { VclaimSepData } from '~/models/vclaim'
|
import useIntegrationSepList from '~/handlers/integration-sep-list.handler'
|
||||||
|
|
||||||
// Services
|
// use handler to provide state and functions
|
||||||
import { getList as geMonitoringVisitList } from '~/services/vclaim-monitoring-visit.service'
|
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('')
|
// expose provides so component can also use provide/inject if needed
|
||||||
const dateRange = ref('12 Agustus 2025 - 31 Agustus 2025')
|
provide('rec_id', recId)
|
||||||
const open = ref(false)
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
const sepData = {
|
provide('table_data_loader', isLoading)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => recAction.value,
|
[recId, recAction],
|
||||||
() => {
|
() => {
|
||||||
if (recAction.value === 'showConfirmDel') {
|
if (recAction.value === 'showConfirmDel') {
|
||||||
open.value = true
|
open.value = true
|
||||||
@@ -153,14 +57,26 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => dateSelection.value,
|
||||||
|
(val) => {
|
||||||
|
if (!val) return
|
||||||
|
setDateRange()
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => serviceType.value,
|
||||||
|
() => {
|
||||||
|
getSepList()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
setServiceTypes()
|
||||||
getSepList()
|
getSepList()
|
||||||
})
|
})
|
||||||
|
|
||||||
provide('rec_id', recId)
|
|
||||||
provide('rec_action', recAction)
|
|
||||||
provide('rec_item', recItem)
|
|
||||||
provide('table_data_loader', isLoading)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -169,100 +85,77 @@ provide('table_data_loader', isLoading)
|
|||||||
<!-- Filter Bar -->
|
<!-- Filter Bar -->
|
||||||
<div class="my-2 flex flex-wrap items-center gap-2">
|
<div class="my-2 flex flex-wrap items-center gap-2">
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<Input
|
<Input v-model="search" placeholder="Cari No. SEP / No. Kartu BPJS..." class="w-72" />
|
||||||
v-model="search"
|
|
||||||
placeholder="Cari No. SEP / No. Kartu BPJS..."
|
<!-- Filter -->
|
||||||
class="w-72"
|
<div class="w-72">
|
||||||
/>
|
<Select id="serviceType" icon-name="i-lucide-chevron-down" v-model="serviceType" :items="serviceTypesList"
|
||||||
|
placeholder="Pilih Pelayanan" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Date Range -->
|
<!-- Date Range -->
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Button
|
<Button variant="outline" class="h-[40px] w-72 border-gray-400 bg-white text-right font-normal">
|
||||||
variant="outline"
|
|
||||||
class="h-[40px] w-72 border-gray-400 bg-white text-right font-normal"
|
|
||||||
>
|
|
||||||
{{ dateRange }}
|
{{ dateRange }}
|
||||||
<Icon
|
<Icon name="i-lucide-calendar" class="h-5 w-5" />
|
||||||
name="i-lucide-calendar"
|
|
||||||
class="h-5 w-5"
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="p-2">
|
<PopoverContent class="p-2">
|
||||||
<Calendar mode="range" />
|
<RangeCalendar v-model="dateSelection" />
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<!-- Export -->
|
<!-- Export -->
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<Button
|
<Button variant="outline"
|
||||||
variant="outline"
|
class="ml-auto h-[40px] w-[120px] rounded-md border-green-600 text-green-600 hover:bg-green-50">
|
||||||
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" />
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="i-lucide-download"
|
|
||||||
class="h-5 w-5"
|
|
||||||
/>
|
|
||||||
Ekspor
|
Ekspor
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent class="w-40">
|
<DropdownMenuContent class="w-40">
|
||||||
<DropdownMenuItem @click="exportCsv">Ekspor CSV</DropdownMenuItem>
|
<DropdownMenuItem @click="handleExportCsv">Ekspor CSV</DropdownMenuItem>
|
||||||
<DropdownMenuItem @click="exportExcel">Ekspor Excel</DropdownMenuItem>
|
<DropdownMenuItem @click="handleExportExcel">Ekspor Excel</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<AppSepList
|
<AppSepList v-if="!isLoading.dataListLoading" :data="data" @update:modelValue="handleRowSelected" />
|
||||||
v-if="!isLoading.dataListLoading"
|
|
||||||
:data="data"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<template v-if="paginationMeta">
|
<template v-if="paginationMeta">
|
||||||
<div v-if="paginationMeta.totalPage > 1">
|
<div v-if="paginationMeta.totalPage > 1">
|
||||||
<PubMyUiPagination
|
<PubMyUiPagination :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
:pagination-meta="paginationMeta"
|
|
||||||
@page-change="handlePageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Trigger button -->
|
<!-- Trigger button -->
|
||||||
<Dialog v-model:open="open">
|
<Dialog v-model:open="open">
|
||||||
<DialogTrigger as-child></DialogTrigger>
|
<DialogTrigger as-child></DialogTrigger>
|
||||||
|
|
||||||
<DialogContent class="sm:max-w-md">
|
<DialogContent class="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Hapus SEP</DialogTitle>
|
<DialogTitle>Hapus SEP</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogDescription class="text-gray-700">Apakah anda yakin ingin menghapus SEP dengan data:</DialogDescription>
|
<DialogDescription class="text-gray-700">Apakah anda yakin ingin menghapus SEP dengan data:</DialogDescription>
|
||||||
|
|
||||||
<div class="mt-4 space-y-2 text-sm">
|
<div class="mt-4 space-y-2 text-sm">
|
||||||
<p>No. SEP : {{ sepData.no_sep }}</p>
|
<p><strong>No. SEP:</strong> {{ sepData.sepNumber }}</p>
|
||||||
<p>No. Kartu BPJS : {{ sepData.kartu }}</p>
|
<p><strong>No. Kartu BPJS:</strong> {{ sepData.cardNumber }}</p>
|
||||||
<p>Nama Pasien : {{ sepData.nama }}</p>
|
<p><strong>Nama Pasien:</strong> {{ sepData.patientName }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter class="mt-6 flex justify-end gap-3">
|
<DialogFooter class="mt-6 flex justify-end gap-3">
|
||||||
<Button
|
<Button variant="outline" class="border-green-600 text-green-600 hover:bg-green-50" @click="() => {
|
||||||
variant="outline"
|
recId = 0
|
||||||
class="border-green-600 text-green-600 hover:bg-green-50"
|
recAction = ''
|
||||||
@click="open = false"
|
open = false
|
||||||
>
|
}">
|
||||||
<X class="mr-1 h-4 w-4" />
|
<X class="mr-1 h-4 w-4" />
|
||||||
Tidak
|
Tidak
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="destructive" class="bg-red-600 hover:bg-red-700" @click="handleRemove">
|
||||||
variant="destructive"
|
|
||||||
class="bg-red-600 hover:bg-red-700"
|
|
||||||
@click="handleDelete"
|
|
||||||
>
|
|
||||||
<Check class="mr-1 h-4 w-4" />
|
<Check class="mr-1 h-4 w-4" />
|
||||||
Ya
|
Ya
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -24,12 +24,7 @@ const ActiveForm = computed(() => formMap[type.value] || EarlyForm)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<SoapiList
|
<SoapiList v-if="mode === 'list'" />
|
||||||
v-if="mode === 'list'"
|
|
||||||
@add="goToEntry"
|
|
||||||
@edit="goToEntry"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<component
|
<component
|
||||||
v-else
|
v-else
|
||||||
:is="ActiveForm"
|
:is="ActiveForm"
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { EarlySchema } from '~/schemas/soapi.schema'
|
|||||||
import { toast } from '~/components/pub/ui/toast'
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
||||||
|
|
||||||
const { backToList } = useQueryMode('mode')
|
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||||
|
const { recordId } = useQueryCRUDMode('record-id')
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const isOpenProcedure = ref(false)
|
const isOpenProcedure = ref(false)
|
||||||
const isOpenDiagnose = ref(false)
|
const isOpenDiagnose = ref(false)
|
||||||
@@ -20,7 +21,7 @@ const schema = EarlySchema
|
|||||||
const payload = ref({
|
const payload = ref({
|
||||||
encounter_id: 0,
|
encounter_id: 0,
|
||||||
time: '',
|
time: '',
|
||||||
typeCode: 'early',
|
typeCode: 'early-medic',
|
||||||
value: '',
|
value: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ const isLoading = reactive<DataTableLoader>({
|
|||||||
|
|
||||||
async function getDiagnoses() {
|
async function getDiagnoses() {
|
||||||
isLoading.isTableLoading = true
|
isLoading.isTableLoading = true
|
||||||
const resp = await xfetch('/api/v1/diagnose-src')
|
const resp = await xfetch(`/api/v1/soapi/${recordId}`)
|
||||||
if (resp.success) {
|
if (resp.success) {
|
||||||
diagnoses.value = (resp.body as Record<string, any>).data
|
diagnoses.value = (resp.body as Record<string, any>).data
|
||||||
}
|
}
|
||||||
@@ -77,6 +78,7 @@ function handleOpen(type: string) {
|
|||||||
|
|
||||||
const entryRef = ref()
|
const entryRef = ref()
|
||||||
async function actionHandler(type: string) {
|
async function actionHandler(type: string) {
|
||||||
|
console.log(type)
|
||||||
if (type === 'back') {
|
if (type === 'back') {
|
||||||
backToList()
|
backToList()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,10 +22,14 @@ interface Props {
|
|||||||
encounter: Encounter
|
encounter: Encounter
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
const route = useRoute()
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const emits = defineEmits(['add', 'edit'])
|
const emits = defineEmits(['add', 'edit'])
|
||||||
|
|
||||||
|
const { recordId } = useQueryCRUDRecordId()
|
||||||
|
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const data = ref([])
|
const data = ref([])
|
||||||
const encounterId = ref<number>(props?.encounter?.id || 0)
|
const encounterId = ref<number>(props?.encounter?.id || 0)
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
@@ -56,13 +60,10 @@ const hreaderPrep: HeaderPrep = {
|
|||||||
icon: 'i-lucide-users',
|
icon: 'i-lucide-users',
|
||||||
addNav: {
|
addNav: {
|
||||||
label: 'Tambah',
|
label: 'Tambah',
|
||||||
onClick: () => emits('add'),
|
onClick: () => goToEntry(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const { recordId } = useQueryCRUDRecordId()
|
|
||||||
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
|
||||||
|
|
||||||
const type = computed(() => (route.query.tab as string) || 'early-medical-assessment')
|
const type = computed(() => (route.query.tab as string) || 'early-medical-assessment')
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -70,13 +71,24 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function getMyList() {
|
async function getMyList() {
|
||||||
const url = `/api/v1/soapi?type-code=${typeCode.value}?includes=encounter`
|
const url = `/api/v1/soapi?typeCode=${typeCode.value}&includes=encounter,employee&encounter-id=${route.params.id}`
|
||||||
const resp = await xfetch(url)
|
const resp = await xfetch(url)
|
||||||
if (resp.success) {
|
if (resp.success) {
|
||||||
data.value = (resp.body as Record<string, any>).data
|
data.value = (resp.body as Record<string, any>).data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goEdit = (id: string) => {
|
||||||
|
router.replace({
|
||||||
|
path: route.path,
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
mode: 'entry',
|
||||||
|
'record-id': id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function handlePageChange(page: number) {
|
function handlePageChange(page: number) {
|
||||||
emits('pageChange', page)
|
emits('pageChange', page)
|
||||||
}
|
}
|
||||||
@@ -142,6 +154,14 @@ watch([recId, recAction], () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// watch(recId, () => {
|
||||||
|
// console.log('recId', recId.value)
|
||||||
|
// console.log('recIdaacin', recAction.value)
|
||||||
|
// if (recAction.value === 'showEdit') {
|
||||||
|
// goEdit(recId.value)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
provide('rec_id', recId)
|
provide('rec_id', recId)
|
||||||
provide('rec_action', recAction)
|
provide('rec_action', recAction)
|
||||||
provide('rec_item', recItem)
|
provide('rec_item', recItem)
|
||||||
|
|||||||
@@ -48,9 +48,18 @@ async function setMenu() {
|
|||||||
const activeRoleParts = activeRole ? activeRole.split('|') : []
|
const activeRoleParts = activeRole ? activeRole.split('|') : []
|
||||||
const role = activeRoleParts[0]+(activeRoleParts.length > 1 ? `-${activeRoleParts[1]}` : '')
|
const role = activeRoleParts[0]+(activeRoleParts.length > 1 ? `-${activeRoleParts[1]}` : '')
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const res = await fetch(`/side-menu-items/${role.toLowerCase()}.json`)
|
const res = await fetch(`/side-menu-items/${role.toLowerCase()}.json`)
|
||||||
const rawMenu = await res.text()
|
const rawMenu = await res.text()
|
||||||
navMenu.value = JSON.parse(rawMenu)
|
const parsedMenu = JSON.parse(rawMenu)
|
||||||
|
|
||||||
|
const { user } = useUserStore()
|
||||||
|
if(user.unit_code == 'rehab') {
|
||||||
|
parsedMenu[0].heading = 'Rehab Medik'
|
||||||
|
parsedMenu[0].items = parsedMenu[0].items.filter((item: any) => item.title != 'IGD')
|
||||||
|
}
|
||||||
|
|
||||||
|
navMenu.value = parsedMenu
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const res = await fetch(`/side-menu-items/blank.json`)
|
const res = await fetch(`/side-menu-items/blank.json`)
|
||||||
const rawMenu = await res.text()
|
const rawMenu = await res.text()
|
||||||
|
|||||||
@@ -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
|
label: string
|
||||||
component?: any
|
component?: any
|
||||||
groups?: string[]
|
groups?: string[]
|
||||||
|
classCode?: string[]
|
||||||
|
subClassCode?: string[]
|
||||||
props?: Record<string, any>
|
props?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Config } from './index'
|
||||||
|
import { refDebounced } from '@vueuse/core'
|
||||||
|
|
||||||
|
const props = defineProps<Config>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:searchModelValue': [value: string]
|
||||||
|
'search': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Quick search
|
||||||
|
const search = ref(props.quickSearchNav?.modelValue || props.refSearchNav?.modelValue || '')
|
||||||
|
const debouncedSearch = refDebounced(search, props.quickSearchNav?.debounceDuration || 500)
|
||||||
|
|
||||||
|
// Computed search model for v-model
|
||||||
|
const searchModel = computed({
|
||||||
|
get: () => search.value,
|
||||||
|
set: (value: string) => {
|
||||||
|
search.value = value
|
||||||
|
emit('update:searchModelValue', value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for external changes to modelValue
|
||||||
|
watch(() => props.quickSearchNav?.modelValue, (newValue) => {
|
||||||
|
if (newValue !== props.quickSearchNav?.modelValue) {
|
||||||
|
search.value = newValue || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch debounced search and emit search event
|
||||||
|
watch(debouncedSearch, (newValue) => {
|
||||||
|
const minLength = props.quickSearchNav?.minLength || 3
|
||||||
|
// Only search if meets minimum length or empty (to clear search)
|
||||||
|
if (newValue.length === 0 || newValue.length >= minLength) {
|
||||||
|
emit('search', newValue)
|
||||||
|
props.refSearchNav?.onInput(newValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle clear search
|
||||||
|
function clearSearch() {
|
||||||
|
searchModel.value = ''
|
||||||
|
props.quickSearchNav?.onClear()
|
||||||
|
props.refSearchNav?.onClear()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between pb-4 2xl:pb-5 ">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="ml-3 text-lg font-semibold text-gray-900">
|
||||||
|
<Icon v-if="icon" :name="icon" class="mr-2 size-4 md:size-6 align-middle" />
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center [&>*]:ms-2">
|
||||||
|
<!-- Slot -->
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<!-- Search Section -->
|
||||||
|
<div v-if="quickSearchNav || refSearchNav" class="relative">
|
||||||
|
<Input
|
||||||
|
v-model="searchModel"
|
||||||
|
name="search"
|
||||||
|
type="text"
|
||||||
|
:class="quickSearchNav?.inputClass || refSearchNav?.inputClass"
|
||||||
|
:placeholder="quickSearchNav?.placeholder || refSearchNav?.placeholder || 'Cari (min. 3 karakter)...'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="search.length > 0"
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||||
|
type="button"
|
||||||
|
@click="clearSearch"
|
||||||
|
>
|
||||||
|
<Icon name="i-lucide-x" class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="quickSearchNav && quickSearchNav.showValidationFeedback !== false && searchModel.length > 0 && searchModel.length < (quickSearchNav.minLength || 3)"
|
||||||
|
class="absolute -bottom-6 left-0 text-xs text-amber-600 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Minimal {{ quickSearchNav.minLength || 3 }} karakter untuk mencari
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Button -->
|
||||||
|
<div v-if="addNav" class="flex items-center">
|
||||||
|
<Button
|
||||||
|
class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm"
|
||||||
|
:class="addNav.classVal"
|
||||||
|
:variant="(addNav.variant as any) || 'default'"
|
||||||
|
@click="addNav?.onClick"
|
||||||
|
>
|
||||||
|
<Icon :name="addNav.icon || 'i-lucide-plus'" class="mr-2 h-4 w-4 align-middle" />
|
||||||
|
{{ addNav.label }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Button -->
|
||||||
|
<div v-if="filterNav" class="flex items-center">
|
||||||
|
<Button
|
||||||
|
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
||||||
|
:class="filterNav.classVal"
|
||||||
|
:variant="(filterNav.variant as any) || 'default'"
|
||||||
|
@click="filterNav?.onClick"
|
||||||
|
>
|
||||||
|
<Icon :name="filterNav.icon || 'i-lucide-filter'" class="mr-2 h-4 w-4 align-middle" />
|
||||||
|
{{ filterNav.label }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Print Button -->
|
||||||
|
<div v-if="printNav" class="flex items-center">
|
||||||
|
<Button
|
||||||
|
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
||||||
|
:class="printNav.classVal"
|
||||||
|
:variant="(printNav.variant as any) || 'default'"
|
||||||
|
@click="printNav?.onClick"
|
||||||
|
>
|
||||||
|
<Icon :name="printNav.icon || 'i-lucide-printer'" class="mr-2 h-4 w-4 align-middle" />
|
||||||
|
{{ printNav.label }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
export type ComponentWithProps = { component: Component, props: Record<string, any> }
|
||||||
|
|
||||||
|
export interface ButtonNav {
|
||||||
|
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
|
||||||
|
classVal?: string
|
||||||
|
classValExt?: string
|
||||||
|
icon?: string
|
||||||
|
label: string
|
||||||
|
onClick?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// can type directly
|
||||||
|
export interface QuickSearchNav {
|
||||||
|
modelValue?: string
|
||||||
|
placeholder?: string
|
||||||
|
inputClass?: string
|
||||||
|
inputPlaceHolder?: string
|
||||||
|
minLength?: number
|
||||||
|
btnClass?: string
|
||||||
|
btnIcon?: string
|
||||||
|
btnLabel?: string
|
||||||
|
showValidationFeedback?: boolean
|
||||||
|
debounceDuration?: number
|
||||||
|
searchParams: object
|
||||||
|
onSubmit?: (searchParams: object) => void
|
||||||
|
onClear: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// callback on event
|
||||||
|
export interface RefSearchNav {
|
||||||
|
modelValue?: string
|
||||||
|
placeholder?: string
|
||||||
|
inputClass?: string
|
||||||
|
inputPlaceHolder?: string
|
||||||
|
btnClass?: string
|
||||||
|
btnIcon?: string
|
||||||
|
onInput: (val: string) => void
|
||||||
|
onClick: () => void
|
||||||
|
onClear: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefExportNav {
|
||||||
|
onExportPdf?: () => void
|
||||||
|
onExportCsv?: () => void
|
||||||
|
onExportExcel?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
title?: string
|
||||||
|
icon?: string
|
||||||
|
components?: ComponentWithProps[]
|
||||||
|
quickSearchNav?: QuickSearchNav
|
||||||
|
refSearchNav?: RefSearchNav // either ref or quick
|
||||||
|
filterNav?: ButtonNav
|
||||||
|
addNav?: ButtonNav
|
||||||
|
printNav?: ButtonNav
|
||||||
|
}
|
||||||
|
|
||||||
|
export { default as ContentHeader } from './content-header.vue'
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
height?: number
|
||||||
|
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 || 200}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 grow
|
||||||
|
}
|
||||||
|
.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>
|
||||||
@@ -71,6 +71,7 @@ export interface KeyNames {
|
|||||||
|
|
||||||
export interface LinkItem {
|
export interface LinkItem {
|
||||||
label: string
|
label: string
|
||||||
|
value?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
href?: string // to cover the needs of stating full external origins full url
|
href?: string // to cover the needs of stating full external origins full url
|
||||||
action?: string // for local paths
|
action?: string // for local paths
|
||||||
@@ -84,6 +85,7 @@ export const ActionEvents = {
|
|||||||
showEdit: 'showEdit',
|
showEdit: 'showEdit',
|
||||||
showDetail: 'showDetail',
|
showDetail: 'showDetail',
|
||||||
showProcess: 'showProcess',
|
showProcess: 'showProcess',
|
||||||
|
showCancel: 'showCancel',
|
||||||
showVerify: 'showVerify',
|
showVerify: 'showVerify',
|
||||||
showConfirmVerify: 'showConfirmVerify',
|
showConfirmVerify: 'showConfirmVerify',
|
||||||
showValidate: 'showValidate',
|
showValidate: 'showValidate',
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
const { class: classProp } = defineProps<{
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-5 text-center">:</div>
|
<div :class="`w-4 ps-1 ${classProp}`">:</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type EncounterItem } from "~/handlers/encounter-init.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="flex">
|
||||||
|
<!-- Menu Sidebar -->
|
||||||
|
<div v-if="data.length > 0" class="w-56 2xl:w-64 flex-shrink-0 rounded-md border bg-white dark:bg-slate-800 shadow-sm">
|
||||||
|
<div class="max-h-[calc(100vh-12rem)] overflow-y-auto px-2 py-3">
|
||||||
|
<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-xs 2xl: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 class="p-4 2xl:p-5 flex-grow">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -20,7 +20,7 @@ function onClick(type: ClickType) {
|
|||||||
@click="onClick('draft')"
|
@click="onClick('draft')"
|
||||||
class="flex items-center gap-2 rounded-full border border-orange-400 bg-orange-50 px-3 py-1 text-sm font-medium text-orange-600 hover:bg-orange-100"
|
class="flex items-center gap-2 rounded-full border border-orange-400 bg-orange-50 px-3 py-1 text-sm font-medium text-orange-600 hover:bg-orange-100"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
{{ props.label }}
|
{{ props.label }}
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ function btnClick() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
<!-- For components as slots -->
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<!-- For components passed by props -->
|
||||||
<div v-if="prep.components">
|
<div v-if="prep.components">
|
||||||
<template v-for="cwp in prep.components">
|
<template v-for="cwp in prep.components">
|
||||||
<component
|
<component
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function btnClick() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="prep.addNav" class="m-2 flex items-center">
|
<div v-if="prep.addNav" class="m-2 flex items-center">
|
||||||
<Button size="md" class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm" @click="btnClick">
|
<Button size="default" class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm" @click="btnClick">
|
||||||
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle" />
|
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle" />
|
||||||
{{ prep.addNav.label }}
|
{{ prep.addNav.label }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+24
-16
@@ -1,4 +1,5 @@
|
|||||||
import type { Permission, RoleAccess } from '~/models/role'
|
import type { Permission, RoleAccesses } from '~/models/role'
|
||||||
|
import { systemCode } from '~/const/common/role'
|
||||||
|
|
||||||
export interface PageOperationPermission {
|
export interface PageOperationPermission {
|
||||||
canRead: boolean
|
canRead: boolean
|
||||||
@@ -7,7 +8,6 @@ export interface PageOperationPermission {
|
|||||||
canDelete: boolean
|
canDelete: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user has access to a page
|
* Check if user has access to a page
|
||||||
*/
|
*/
|
||||||
@@ -15,19 +15,27 @@ export function useRBAC() {
|
|||||||
// NOTE: this roles was dummy for testing only, it should taken from the user store
|
// NOTE: this roles was dummy for testing only, it should taken from the user store
|
||||||
const authStore = useUserStore()
|
const authStore = useUserStore()
|
||||||
|
|
||||||
const checkRole = (roleAccess: RoleAccess, _userRoles?: string[]): boolean => {
|
const checkRole = (roleAccesses: RoleAccesses, _userRoles?: string[]): boolean => {
|
||||||
const roles = authStore.userRole
|
const activeRole = authStore.getActiveRole() || ''
|
||||||
return roles.some((role: string) => (role in roleAccess) || role === 'system') // system by-passes this check
|
if (activeRole === systemCode) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return (activeRole in roleAccesses);
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkPermission = (roleAccess: RoleAccess, permission: Permission, _userRoles?: string[]): boolean => {
|
const checkPermission = (roleAccesses: RoleAccesses, permission: Permission, _userRoles?: string[]): boolean => {
|
||||||
const roles = authStore.userRole
|
const activeRole = authStore.getActiveRole() || ''
|
||||||
return roles.some((role: string) => roleAccess[role]?.includes(permission) || role === 'system') // system by-passes this check
|
if (activeRole === systemCode) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (activeRole in roleAccesses && roleAccesses[activeRole]) {
|
||||||
|
return roleAccesses[activeRole].includes(permission)
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserPermissions = (roleAccess: RoleAccess, _userRoles?: string[]): Permission[] => {
|
const getUserPermissions = (roleAccess: RoleAccesses, _userRoles?: string[]): Permission[] => {
|
||||||
const roles = authStore.userRole
|
const roles = authStore.userRoles
|
||||||
// const roles = ['admisi']
|
|
||||||
const permissions = new Set<Permission>()
|
const permissions = new Set<Permission>()
|
||||||
|
|
||||||
roles.forEach((role: string) => {
|
roles.forEach((role: string) => {
|
||||||
@@ -39,12 +47,12 @@ export function useRBAC() {
|
|||||||
return Array.from(permissions)
|
return Array.from(permissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasCreateAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'C')
|
const hasCreateAccess = (roleAccess: RoleAccesses) => checkPermission(roleAccess, 'C')
|
||||||
const hasReadAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'R')
|
const hasReadAccess = (roleAccess: RoleAccesses) => checkPermission(roleAccess, 'R')
|
||||||
const hasUpdateAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'U')
|
const hasUpdateAccess = (roleAccess: RoleAccesses) => checkPermission(roleAccess, 'U')
|
||||||
const hasDeleteAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'D')
|
const hasDeleteAccess = (roleAccess: RoleAccesses) => checkPermission(roleAccess, 'D')
|
||||||
|
|
||||||
const getPagePermissions = (roleAccess: RoleAccess): PageOperationPermission => ({
|
const getPagePermissions = (roleAccess: RoleAccesses): PageOperationPermission => ({
|
||||||
canRead : hasReadAccess(roleAccess),
|
canRead : hasReadAccess(roleAccess),
|
||||||
canCreate: hasCreateAccess(roleAccess),
|
canCreate: hasCreateAccess(roleAccess),
|
||||||
canUpdate: hasUpdateAccess(roleAccess),
|
canUpdate: hasUpdateAccess(roleAccess),
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export const systemCode = 'system'
|
||||||
|
|
||||||
|
export const rehabInstCode = 'rehab'
|
||||||
|
export const rehabUnitCode = 'rehab'
|
||||||
|
|
||||||
|
export const headPosCode = 'head' // head position
|
||||||
|
export const respPosCode = 'resp' // responsible position, verificator
|
||||||
|
|
||||||
|
export type UnitLevel =
|
||||||
|
'inst' | // installation
|
||||||
|
'unit' | // unit / poly
|
||||||
|
'spec' | // specialist
|
||||||
|
'subspec' // subspecialist
|
||||||
|
|
||||||
|
export const medicalRoles = [
|
||||||
|
'emp|doc', // doctor
|
||||||
|
'emp|nur', // nurse
|
||||||
|
'emp|miw', // midwife
|
||||||
|
'emp|thr', // therapist
|
||||||
|
'emp|nut', // nutritionist
|
||||||
|
'emp|pha', // pharmacy
|
||||||
|
'emp|lab' // laborant
|
||||||
|
]
|
||||||
|
|
||||||
|
export const serviceRoles = [
|
||||||
|
'emp|reg',
|
||||||
|
...medicalRoles,
|
||||||
|
]
|
||||||
|
|
||||||
|
export function genSpecHeadCode(unit_level: UnitLevel, unit_code: string): string {
|
||||||
|
return `${unit_level}|${unit_code}|${headPosCode}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genUnitRespCode(unit_level: UnitLevel, unit_code: string): string {
|
||||||
|
return `${unit_level}|${unit_code}|${respPosCode}`
|
||||||
|
}
|
||||||
@@ -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[]>> = {
|
||||||
|
'/ambulatory/registration-queue': {
|
||||||
|
'emp|reg': ['R', 'U', 'D'],
|
||||||
|
},
|
||||||
|
'/ambulatory/encounter-queue': {
|
||||||
|
'emp|nur': ['R', 'U', 'D'],
|
||||||
|
},
|
||||||
|
'/ambulatory/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'],
|
||||||
|
},
|
||||||
|
'/ambulatory/encounter/add': {
|
||||||
|
'emp|reg': ['C', 'R', 'U', 'D'],
|
||||||
|
},
|
||||||
|
'/ambulatory/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'],
|
||||||
|
},
|
||||||
|
'/ambulatory/encounter/[id]/edit': {
|
||||||
|
'emp|reg': ['C', 'R', 'U', 'D'],
|
||||||
|
},
|
||||||
|
'/ambulatory/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'],
|
||||||
|
},
|
||||||
|
'/ambulatory/consulation': {
|
||||||
|
'emp|doc': ['R'],
|
||||||
|
},
|
||||||
|
'/ambulatory/consulation/[id]/process': {
|
||||||
|
'emp|doc': ['R', 'U'],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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[]>> = {
|
||||||
|
'/chemotherapy/adm': {
|
||||||
|
'emp|reg': ['R'],
|
||||||
|
'emp|nur': ['R', 'U', 'D'],
|
||||||
|
},
|
||||||
|
'/outpatient/series': {
|
||||||
|
'emp|nur': ['R', 'U', 'D'],
|
||||||
|
'emp|doc': ['R', 'U', 'D'],
|
||||||
|
'emp|thr': ['R', 'U', 'D'],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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'],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,491 @@
|
|||||||
|
import { isValidDate } from '~/lib/date'
|
||||||
|
import { medicalRoles } from '~/const/common/role'
|
||||||
|
|
||||||
|
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,
|
||||||
|
) {
|
||||||
|
console.log(props)
|
||||||
|
// const normalClassCode = props.classCode === 'ambulatory' ? 'outpatient' : props.classCode
|
||||||
|
const normalClassCode = props.classCode === 'ambulatory' ? 'ambulatory' : 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|ambulatory': {
|
||||||
|
'unit|rehab': {
|
||||||
|
items: listItemsForOutpatientRehab,
|
||||||
|
roles: medicalRoles,
|
||||||
|
},
|
||||||
|
'unit|chemo': {
|
||||||
|
items: listItemsForOutpatientChemo,
|
||||||
|
roles: medicalRoles,
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -2,7 +2,12 @@
|
|||||||
import CardContent from '~/components/pub/ui/card/CardContent.vue'
|
import CardContent from '~/components/pub/ui/card/CardContent.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
|
||||||
const contentFrame = computed(() => route.meta.contentFrame)
|
const contentFrame = computed(() => route.meta.contentFrame)
|
||||||
|
const contentPadding = computed(() => route.meta.contentPadding || 'p-4 2xl:p-5')
|
||||||
|
const contentUseCard = computed(() => route.meta.contentUseCard === false ? false : true)
|
||||||
|
console.log(route.meta.contentUseCard,contentUseCard)
|
||||||
const contentFrameClass = computed(() => {
|
const contentFrameClass = computed(() => {
|
||||||
switch (contentFrame.value) {
|
switch (contentFrame.value) {
|
||||||
case 'cf-container-2xl':
|
case 'cf-container-2xl':
|
||||||
@@ -28,13 +33,14 @@ const contentFrameClass = computed(() => {
|
|||||||
<LayoutAppSidebar />
|
<LayoutAppSidebar />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<LayoutHeader />
|
<LayoutHeader />
|
||||||
<div :class="`w-full p-4 2xl:p-5 flex justify-center ${contentFrameClass}`">
|
<div :class="`w-full flex justify-center ${contentPadding} ${contentFrameClass}`">
|
||||||
<div v-if="contentFrame !== 'cf-no-frame'">
|
<div v-if="contentFrame !== 'cf-no-frame'">
|
||||||
<Card>
|
<Card v-if="contentUseCard">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<slot />
|
<slot />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<slot v-else />
|
||||||
</div>
|
</div>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const dataStatusCodes: Record<string, string> = {
|
|||||||
review: 'Review',
|
review: 'Review',
|
||||||
process: 'Proses',
|
process: 'Proses',
|
||||||
done: 'Selesai',
|
done: 'Selesai',
|
||||||
canceled: 'Dibatalkan',
|
cancel: 'Dibatalkan',
|
||||||
rejected: 'Ditolak',
|
rejected: 'Ditolak',
|
||||||
skiped: 'Dilewati',
|
skiped: 'Dilewati',
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-36
@@ -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 } {
|
export function getAge(dateString: string, comparedDate?: string): { idFormat: string; extFormat: string } {
|
||||||
const birthDate = new Date(dateString);
|
const birthDate = new Date(dateString)
|
||||||
const today = new Date();
|
const today = new Date()
|
||||||
|
|
||||||
if (comparedDate) {
|
if (comparedDate) {
|
||||||
const comparedDateObj = new Date(comparedDate);
|
const comparedDateObj = new Date(comparedDate)
|
||||||
today.setFullYear(comparedDateObj.getFullYear());
|
today.setFullYear(comparedDateObj.getFullYear())
|
||||||
today.setMonth(comparedDateObj.getMonth());
|
today.setMonth(comparedDateObj.getMonth())
|
||||||
today.setDate(comparedDateObj.getDate());
|
today.setDate(comparedDateObj.getDate())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the date part
|
// Format the date part
|
||||||
const options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', year: 'numeric' };
|
const options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', year: 'numeric' }
|
||||||
const idFormat = birthDate.toLocaleDateString('id-ID', options);
|
const idFormat = birthDate.toLocaleDateString('id-ID', options)
|
||||||
|
|
||||||
// Calculate age
|
// Calculate age
|
||||||
let years = today.getFullYear() - birthDate.getFullYear();
|
let years = today.getFullYear() - birthDate.getFullYear()
|
||||||
let months = today.getMonth() - birthDate.getMonth();
|
let months = today.getMonth() - birthDate.getMonth()
|
||||||
let days = today.getDate() - birthDate.getDate();
|
let days = today.getDate() - birthDate.getDate()
|
||||||
|
|
||||||
if (months < 0 || (months === 0 && days < 0)) {
|
if (months < 0 || (months === 0 && days < 0)) {
|
||||||
years--;
|
years--
|
||||||
months += 12;
|
months += 12
|
||||||
}
|
}
|
||||||
|
|
||||||
if (days < 0) {
|
if (days < 0) {
|
||||||
const prevMonth = new Date(today.getFullYear(), today.getMonth() - 1, 0);
|
const prevMonth = new Date(today.getFullYear(), today.getMonth() - 1, 0)
|
||||||
days += prevMonth.getDate();
|
days += prevMonth.getDate()
|
||||||
months--;
|
months--
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the age part
|
// Format the age part
|
||||||
let extFormat = '';
|
let extFormat = ''
|
||||||
if ([years, months, days].filter(Boolean).join(' ')) {
|
if ([years, months, days].filter(Boolean).join(' ')) {
|
||||||
extFormat = `${years} Tahun ${months} Bulan ${days} Hari`;
|
extFormat = `${years} Tahun ${months} Bulan ${days} Hari`
|
||||||
} else {
|
} else {
|
||||||
extFormat = '0';
|
extFormat = '0'
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
idFormat,
|
idFormat,
|
||||||
extFormat
|
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 {
|
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 month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
return `${year}-${month}-${day}`;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { RoleAccess } from '~/models/role'
|
import type { RoleAccess } from '~/models/role'
|
||||||
|
|
||||||
export const PAGE_PERMISSIONS = {
|
export const PAGE_PERMISSIONS = {
|
||||||
'/patient': {
|
'/client/patient': {
|
||||||
'emp|doc': ['R'],
|
'emp|doc': ['R'],
|
||||||
'emp|nur': ['R'],
|
'emp|nur': ['R'],
|
||||||
'emp|reg': ['C', 'R', 'U', 'D'],
|
'emp|reg': ['C', 'R', 'U', 'D'],
|
||||||
@@ -9,13 +9,11 @@ export const PAGE_PERMISSIONS = {
|
|||||||
'emp|pay': ['R'],
|
'emp|pay': ['R'],
|
||||||
'emp|mng': ['R'],
|
'emp|mng': ['R'],
|
||||||
},
|
},
|
||||||
'/doctor': {
|
'/human-src/employee': {
|
||||||
'emp|doc': ['C', 'R', 'U', 'D'],
|
'div|hrd': ['C', 'R', 'U', 'D'],
|
||||||
'emp|nur': ['R'],
|
},
|
||||||
'emp|reg': ['R'],
|
'/human-src/intern': {
|
||||||
'emp|pha': ['R'],
|
'div|hrd': ['C', 'R', 'U', 'D'],
|
||||||
'emp|pay': ['R'],
|
|
||||||
'emp|mng': ['R'],
|
|
||||||
},
|
},
|
||||||
'/satusehat': {
|
'/satusehat': {
|
||||||
'emp|doc': ['R'],
|
'emp|doc': ['R'],
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { medicalRoles, respPosCode } from '~/const/common/role'
|
||||||
|
|
||||||
|
export function getServicePosition(role?: string): string {
|
||||||
|
if(!role) {
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
|
if (medicalRoles.includes(role)) {
|
||||||
|
return 'medical'
|
||||||
|
} else if (role === 'emp|reg') {
|
||||||
|
return 'registration'
|
||||||
|
} else if (role.includes('|resp')) {
|
||||||
|
return 'verificator'
|
||||||
|
} else {
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ export default defineNuxtRouteMiddleware((to) => {
|
|||||||
const requiredRoles = to.meta.roles as string[]
|
const requiredRoles = to.meta.roles as string[]
|
||||||
if (requiredRoles && requiredRoles.length > 0) {
|
if (requiredRoles && requiredRoles.length > 0) {
|
||||||
// FIXME: change this dummy roles, when api is ready
|
// FIXME: change this dummy roles, when api is ready
|
||||||
const userRoles = authStore.userRole
|
const userRoles = authStore.userRoles
|
||||||
// const userRoles = ['admisi']
|
// const userRoles = ['admisi']
|
||||||
const hasRequiredRole = requiredRoles.some((role) => userRoles.includes(role))
|
const hasRequiredRole = requiredRoles.some((role) => userRoles.includes(role))
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface Encounter {
|
|||||||
discharge_date?: string
|
discharge_date?: string
|
||||||
internalReferences?: InternalReference[]
|
internalReferences?: InternalReference[]
|
||||||
deathCause?: DeathCause
|
deathCause?: DeathCause
|
||||||
|
paymentMethod_code?: string
|
||||||
status_code: string
|
status_code: string
|
||||||
encounterDocuments: EncounterDocument[]
|
encounterDocuments: EncounterDocument[]
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -14,9 +14,9 @@ export interface AuthState {
|
|||||||
|
|
||||||
export type Permission = 'C' | 'R' | 'U' | 'D'
|
export type Permission = 'C' | 'R' | 'U' | 'D'
|
||||||
|
|
||||||
export interface RoleAccess {
|
export interface RoleAccesses {
|
||||||
[role: string]: Permission[]
|
[role: string]: Permission[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PagePath = keyof typeof PAGE_PERMISSIONS
|
// export type PagePath = keyof typeof PAGE_PERMISSIONS
|
||||||
export type PagePermission = (typeof PAGE_PERMISSIONS)[PagePath]
|
// export type PagePermission = (typeof PAGE_PERMISSIONS)[PagePath]
|
||||||
|
|||||||
+22
-15
@@ -1,50 +1,57 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PagePermission } from '~/models/role'
|
// Pubs
|
||||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
|
||||||
|
|
||||||
|
// Models & Consts
|
||||||
|
import type { Permission } from '~/models/role'
|
||||||
|
import { permissions } from '~/const/page-permission/ambulatory'
|
||||||
|
|
||||||
|
// Apps
|
||||||
|
import Content from '~/components/content/encounter/entry.vue'
|
||||||
|
|
||||||
|
// Page meta
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
middleware: ['rbac'],
|
||||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
roles: ['emp|reg'],
|
||||||
title: 'Edit Kunjungan',
|
title: 'Edit Kunjungan',
|
||||||
contentFrame: 'cf-full-width',
|
contentFrame: 'cf-full-width',
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
|
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
|
||||||
})
|
})
|
||||||
|
|
||||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/outpatient/encounter']
|
// Preps role checking
|
||||||
|
const roleAccess: Record<string, Permission[]> = permissions['/ambulatory/encounter/[id]/edit'] || {}
|
||||||
const { checkRole, hasUpdateAccess } = useRBAC()
|
const { checkRole, hasUpdateAccess } = useRBAC()
|
||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page, need to use try - catch for proper handling
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
throw createError({
|
navigateTo('/403')
|
||||||
statusCode: 403,
|
|
||||||
statusMessage: 'Access denied',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canUpdate = hasUpdateAccess(roleAccess)
|
const canUpdate = hasUpdateAccess(roleAccess)
|
||||||
|
|
||||||
// Get encounter ID from route params
|
// Get encounter ID from route params
|
||||||
const encounterId = computed(() => {
|
const encounter_id = computed(() => {
|
||||||
const id = route.params.id
|
const id = route.params.id
|
||||||
return typeof id === 'string' ? parseInt(id) : 0
|
return typeof id === 'string' ? parseInt(id) : 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// User info
|
||||||
|
const { user } = useUserStore()
|
||||||
|
const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="canUpdate">
|
<div v-if="canUpdate">
|
||||||
<ContentEncounterEntry
|
<Content
|
||||||
:id="encounterId"
|
:id="encounter_id"
|
||||||
class-code="ambulatory"
|
class-code="ambulatory"
|
||||||
sub-class-code="reg"
|
:sub-class-code="subClassCode"
|
||||||
form-type="Edit"
|
form-type="Edit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
+21
-15
@@ -1,34 +1,40 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PagePermission } from '~/models/role'
|
// Pubs
|
||||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
|
||||||
|
|
||||||
|
// Models & Consts
|
||||||
|
import type { Permission } from '~/models/role'
|
||||||
|
import { permissions } from '~/const/page-permission/ambulatory'
|
||||||
|
import { medicalRoles } from '~/const/common/role'
|
||||||
|
|
||||||
|
// Page meta
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
middleware: ['rbac'],
|
||||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
roles: medicalRoles,
|
||||||
title: 'Detail Surat Kontrol',
|
title: 'Detail Kunjungan',
|
||||||
contentFrame: 'cf-container-md',
|
contentFrame: 'cf-container-md',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Define common things
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
useHead({
|
// Prep role checking
|
||||||
title: () => route.meta.title as string,
|
const roleAccess: Record<string, Permission[]> = permissions['/ambulatory/encounter/[id]'] || {}
|
||||||
})
|
|
||||||
|
|
||||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
|
||||||
|
|
||||||
const { checkRole, hasReadAccess } = useRBAC()
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
// if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
// navigateTo('/403')
|
navigateTo('/403')
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
// const canRead = hasReadAccess(roleAccess)
|
const canRead = hasReadAccess(roleAccess)
|
||||||
const canRead = true
|
|
||||||
|
// Page needs
|
||||||
|
useHead({
|
||||||
|
title: () => route.meta.title as string,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Pubs
|
||||||
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
|
|
||||||
|
// AppS
|
||||||
|
import type { Permission } from '~/models/role'
|
||||||
|
import { permissions } from '~/const/page-permission/ambulatory'
|
||||||
|
import Content from '~/components/content/encounter/process.vue'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['rbac'],
|
||||||
|
roles: ['emp|doc', 'emp|nur', 'emp|miw', 'emp|nut', 'emp|lab', 'emp|pha', 'emp|thr'],
|
||||||
|
title: 'Proses Kunjungan',
|
||||||
|
contentFrame: 'cf-full-width',
|
||||||
|
contentPadding: 'p-0',
|
||||||
|
contentUseCard: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Preps role checking
|
||||||
|
const roleAccess: Record<string, Permission[]> = permissions['/ambulatory/encounter/[id]/process'] || {}
|
||||||
|
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" />
|
||||||
|
</div>
|
||||||
|
<Error v-else :status-code="403" />
|
||||||
|
</template>
|
||||||
+19
-17
@@ -1,41 +1,43 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PagePermission } from '~/models/role'
|
import type { Permission } from '~/models/role'
|
||||||
|
import { permissions } from '~/const/page-permission/ambulatory'
|
||||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
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({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
middleware: ['rbac'],
|
||||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
roles: ['emp|reg'],
|
||||||
title: 'Tambah Kunjungan',
|
title: 'Tambah Kunjungan',
|
||||||
contentFrame: 'cf-full-width',
|
contentFrame: 'cf-full-width',
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
// Preps role checking
|
||||||
|
const roleAccess: Record<string, Permission[]> = permissions['/ambulatory/encounter/add'] || {}
|
||||||
useHead({
|
const { checkRole, hasReadAccess, hasCreateAccess } = useRBAC()
|
||||||
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
|
|
||||||
})
|
|
||||||
|
|
||||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/outpatient/encounter']
|
|
||||||
|
|
||||||
const { checkRole, hasCreateAccess } = useRBAC()
|
|
||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
throw createError({
|
navigateTo('/403')
|
||||||
statusCode: 403,
|
|
||||||
statusMessage: 'Access denied',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Define permission-based computed properties
|
||||||
const canCreate = hasCreateAccess(roleAccess)
|
const canCreate = hasCreateAccess(roleAccess)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="canCreate">
|
<div v-if="canCreate">
|
||||||
<ContentEncounterEntry
|
<Content
|
||||||
:id="0"
|
:id="0"
|
||||||
class-code="ambulatory"
|
class-code="ambulatory"
|
||||||
sub-class-code="reg"
|
sub-class-code="reg"
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Pubs
|
||||||
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
|
|
||||||
|
// Models & Consts
|
||||||
|
import type { Permission } from '~/models/role'
|
||||||
|
import { permissions } from '~/const/page-permission/ambulatory'
|
||||||
|
|
||||||
|
// Apps
|
||||||
|
import Content from '~/components/content/encounter/list.vue'
|
||||||
|
|
||||||
|
// Page meta
|
||||||
|
definePageMeta({
|
||||||
|
// middleware: ['rbac'],
|
||||||
|
// roles: ['emp|reg', 'emp|nur', 'emp|doc', 'emp|miw', 'emp|thr', 'emp|nut', 'emp|pha', 'emp|lab'],
|
||||||
|
title: 'Daftar Kunjungan',
|
||||||
|
contentFrame: 'cf-full-width',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Define common things
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// Preps role checking
|
||||||
|
const roleAccess: Record<string, Permission[]> = permissions[route.path] || {}
|
||||||
|
const { checkRole, hasCreateAccess, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
|
// Check if user has access to this page
|
||||||
|
const hasAccess = checkRole(roleAccess)
|
||||||
|
const canRead = hasReadAccess(roleAccess)
|
||||||
|
if (!hasAccess || !canRead) {
|
||||||
|
navigateTo('/403')
|
||||||
|
}
|
||||||
|
const canCreate = hasCreateAccess(roleAccess)
|
||||||
|
|
||||||
|
// Page needs
|
||||||
|
useHead({
|
||||||
|
title: () => route.meta.title as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
// User info
|
||||||
|
const { user } = useUserStore()
|
||||||
|
const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="canRead">
|
||||||
|
<Content
|
||||||
|
class-code="ambulatory"
|
||||||
|
:sub-class-code="subClassCode"
|
||||||
|
:can-create="canCreate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Error
|
||||||
|
v-else
|
||||||
|
:status-code="403"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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/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>
|
||||||
+20
-18
@@ -1,44 +1,46 @@
|
|||||||
<script setup lang="ts">
|
<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 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({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
middleware: ['rbac'],
|
||||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
roles: ['emp|reg'],
|
||||||
title: 'Tambah Kunjungan',
|
title: 'Tambah Kunjungan',
|
||||||
contentFrame: 'cf-full-width',
|
contentFrame: 'cf-full-width',
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
// Preps role checking
|
||||||
|
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
|
||||||
useHead({
|
const { checkRole, hasReadAccess, hasCreateAccess } = useRBAC()
|
||||||
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
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
throw createError({
|
navigateTo('/403')
|
||||||
statusCode: 403,
|
|
||||||
statusMessage: 'Access denied',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Define permission-based computed properties
|
||||||
const canCreate = hasCreateAccess(roleAccess)
|
const canCreate = hasCreateAccess(roleAccess)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="canCreate">
|
<div v-if="canCreate">
|
||||||
<ContentEncounterEntry
|
<Content
|
||||||
:id="0"
|
:id="0"
|
||||||
class-code="ambulatory"
|
class-code="ambulatory"
|
||||||
sub-class-code="rehab"
|
sub-class-code="chemo"
|
||||||
form-type="Tambah"
|
form-type="Tambah"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
+15
-13
@@ -1,23 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<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 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({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
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',
|
title: 'Daftar Kunjungan',
|
||||||
contentFrame: 'cf-full-width',
|
contentFrame: 'cf-full-width',
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
// Preps role checking
|
||||||
|
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
|
||||||
useHead({
|
|
||||||
title: () => route.meta.title as string,
|
|
||||||
})
|
|
||||||
|
|
||||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/outpatient/encounter']
|
|
||||||
|
|
||||||
const { checkRole, hasReadAccess } = useRBAC()
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
@@ -28,14 +24,20 @@ if (!hasAccess) {
|
|||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canRead = hasReadAccess(roleAccess)
|
const canRead = hasReadAccess(roleAccess)
|
||||||
|
|
||||||
|
// Page needs
|
||||||
|
const route = useRoute()
|
||||||
|
useHead({
|
||||||
|
title: () => route.meta.title as string,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="canRead">
|
<div v-if="canRead">
|
||||||
<ContentEncounterList
|
<Content
|
||||||
class-code="ambulatory"
|
class-code="ambulatory"
|
||||||
sub-class-code="reg"
|
sub-class-code="chemo"
|
||||||
type="encounter"
|
type="encounter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PagePermission } from '~/models/role'
|
import type { Permission } from '~/models/role'
|
||||||
|
import { permissions } from '~/const/page-permission/chemoteraphy'
|
||||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
middleware: ['rbac'],
|
||||||
@@ -16,7 +16,7 @@ useHead({
|
|||||||
title: () => route.meta.title as string,
|
title: () => route.meta.title as string,
|
||||||
})
|
})
|
||||||
|
|
||||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor']
|
const roleAccess: Record<string, Permission[]> = permissions['/chemotherapy'] || {}
|
||||||
|
|
||||||
const { checkRole, hasReadAccess } = useRBAC()
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ if (!hasAccess) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canRead = true // hasReadAccess(roleAccess)
|
const canRead = hasReadAccess(roleAccess)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 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>
|
||||||
+16
-17
@@ -1,23 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<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 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({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
middleware: ['rbac'],
|
||||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||||
title: 'Daftar Dokter',
|
title: 'Kunjungan',
|
||||||
contentFrame: 'cf-full-width',
|
contentFrame: 'cf-full-width',
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
// Preps role checking
|
||||||
|
const roleAccess: Record<string, Permission[]> = permissions['/emergency/encounter'] || {}
|
||||||
useHead({
|
|
||||||
title: () => route.meta.title as string,
|
|
||||||
})
|
|
||||||
|
|
||||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor']
|
|
||||||
|
|
||||||
const { checkRole, hasReadAccess } = useRBAC()
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
@@ -28,13 +23,17 @@ if (!hasAccess) {
|
|||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canRead = hasReadAccess(roleAccess)
|
const canRead = hasReadAccess(roleAccess)
|
||||||
|
|
||||||
|
// Page needs
|
||||||
|
const route = useRoute()
|
||||||
|
useHead({
|
||||||
|
title: () => `${route.meta.title}`,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="canRead">
|
||||||
<div v-if="canRead">
|
<Content class-code="emergency" sub-class-code="emg" />
|
||||||
<ContentDoctorList />
|
|
||||||
</div>
|
|
||||||
<Error v-else :status-code="403" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<Error v-else :status-code="403" />
|
||||||
|
</template>
|
||||||
@@ -1,44 +1,46 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PagePermission } from '~/models/role'
|
import type { Permission } from '~/models/role'
|
||||||
|
import { permissions } from '~/const/page-permission/ambulatory'
|
||||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
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({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
middleware: ['rbac'],
|
||||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
roles: ['emp|reg'],
|
||||||
title: 'Tambah Kunjungan',
|
title: 'Tambah Kunjungan',
|
||||||
contentFrame: 'cf-full-width',
|
contentFrame: 'cf-full-width',
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
// Preps role checking
|
||||||
|
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {}
|
||||||
useHead({
|
const { checkRole, hasReadAccess, hasCreateAccess } = useRBAC()
|
||||||
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
|
|
||||||
})
|
|
||||||
|
|
||||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/emergency/encounter']
|
|
||||||
|
|
||||||
const { checkRole, hasCreateAccess } = useRBAC()
|
|
||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
throw createError({
|
navigateTo('/403')
|
||||||
statusCode: 403,
|
|
||||||
statusMessage: 'Access denied',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Define permission-based computed properties
|
||||||
const canCreate = hasCreateAccess(roleAccess)
|
const canCreate = hasCreateAccess(roleAccess)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="canCreate">
|
<div v-if="canCreate">
|
||||||
<ContentEncounterEntry
|
<Content
|
||||||
:id="0"
|
:id="0"
|
||||||
class-code="emergency"
|
class-code="emergency"
|
||||||
sub-class-code="emg"
|
sub-class-code="reg"
|
||||||
form-type="Tambah"
|
form-type="Tambah"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<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 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({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
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',
|
title: 'Daftar Kunjungan',
|
||||||
contentFrame: 'cf-full-width',
|
contentFrame: 'cf-full-width',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Preps role checking
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const roleAccess: Record<string, Permission[]> = permissions[route.path] || {}
|
||||||
useHead({
|
|
||||||
title: () => route.meta.title as string,
|
|
||||||
})
|
|
||||||
|
|
||||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/emergency/encounter']
|
|
||||||
|
|
||||||
const { checkRole, hasReadAccess } = useRBAC()
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
@@ -28,14 +25,22 @@ if (!hasAccess) {
|
|||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canRead = hasReadAccess(roleAccess)
|
const canRead = hasReadAccess(roleAccess)
|
||||||
|
|
||||||
|
// Page needs
|
||||||
|
useHead({
|
||||||
|
title: () => route.meta.title as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { user } = useUserStore()
|
||||||
|
const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="canRead">
|
<div v-if="canRead">
|
||||||
<ContentEncounterList
|
<Content
|
||||||
class-code="emergency"
|
class-code="emergency"
|
||||||
sub-class-code="emg"
|
:sub-class-code="subClassCode"
|
||||||
type="encounter"
|
type="encounter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user