Merge pull request #191 from dikstub-rssa/feat/page-cleaning

Feat/page cleaning
This commit is contained in:
Munawwirul Jamal
2025-11-28 19:20:57 +07:00
committed by GitHub
56 changed files with 999 additions and 655 deletions
@@ -9,38 +9,38 @@ const props = defineProps<{
const recId = inject<Ref<number>>('rec_id')! const recId = inject<Ref<number>>('rec_id')!
const recAction = inject<Ref<string>>('rec_action')! const recAction = inject<Ref<string>>('rec_action')!
const recItem = inject<Ref<any>>('rec_item')! const recItem = inject<Ref<any>>('rec_item')!
const activeServicePosition = inject<Ref<string>>('activeServicePosition')! // previously: activePosition
const activeKey = ref<string | null>(null) const activeKey = ref<string | null>(null)
const activePosition = inject<Ref<string>>('position')!
const linkItemsFiltered = ref<LinkItem[]>([]) const linkItemsFiltered = ref<LinkItem[]>([])
const linkItemsBase: LinkItem[] = [ const baseLinkItems: LinkItem[] = [
{
label: 'Nothing',
value: 'nothing',
icon: 'i-lucide-file',
},
]
const linkItems: LinkItem[] = [
{ {
label: 'Detail', label: 'Detail',
value: 'detail', value: 'detail',
onClick: () => { onClick: () => {
detail() proceedItem(ActionEvents.showDetail)
}, },
icon: 'i-lucide-eye', icon: 'i-lucide-eye',
}, },
]
const medicalLinkItems: LinkItem[] = [
{ {
label: 'Process', label: 'Process',
value: 'edit', value: 'process',
onClick: () => { onClick: () => {
edit() proceedItem(ActionEvents.showProcess)
}, },
icon: 'i-lucide-pencil', icon: 'i-lucide-shuffle',
}, },
]
const regLinkItems: LinkItem[] = [
{ {
label: 'Print', label: 'Print',
value: 'print', value: 'print',
onClick: () => { onClick: () => {
print() proceedItem(ActionEvents.showPrint)
}, },
icon: 'i-lucide-printer', icon: 'i-lucide-printer',
}, },
@@ -48,7 +48,7 @@ const linkItems: LinkItem[] = [
label: 'Batalkan', label: 'Batalkan',
value: 'cancel', value: 'cancel',
onClick: () => { onClick: () => {
cancel() proceedItem(ActionEvents.showCancel)
}, },
icon: 'i-lucide-circle-x', icon: 'i-lucide-circle-x',
}, },
@@ -56,65 +56,49 @@ const linkItems: LinkItem[] = [
label: 'Hapus', label: 'Hapus',
value: 'remove', value: 'remove',
onClick: () => { onClick: () => {
remove() proceedItem(ActionEvents.showConfirmDelete)
}, },
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
}, },
] ]
function detail() { 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 recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec recItem.value = props.rec
recAction.value = action
} }
function edit() { function getLinks() {
recId.value = props.rec.id || 0 switch (activeServicePosition.value) {
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function print() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showPrint
recItem.value = props.rec
}
function cancel() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showCancel
recItem.value = props.rec
}
function remove() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
linkItemsFiltered.value = [...linkItemsBase]
function getLinks(position: string) {
switch (position) {
case 'medical': case 'medical':
linkItemsFiltered.value = [...linkItems] linkItemsFiltered.value = [...baseLinkItems, ...medicalLinkItems]
break break
case 'verificator': case 'registration':
linkItemsFiltered.value = [ linkItemsFiltered.value = [...baseLinkItems, ...regLinkItems]
...linkItems.filter((item) => ['detail', 'print'].includes(item.value || '')), case 'unit|resp':
] linkItemsFiltered.value = [...baseLinkItems]
break break
default: default:
linkItemsFiltered.value = [...linkItemsBase] linkItemsFiltered.value = voidLinkItems
break break
} }
} }
getLinks(activePosition.value)
watch(activePosition, () => {
getLinks(activePosition.value)
})
</script> </script>
<template> <template>
+123
View File
@@ -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>
+2 -1
View File
@@ -1,4 +1,5 @@
<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<{
@@ -7,7 +8,7 @@ const props = defineProps<{
</script> </script>
<template> <template>
<PubMyUiDataTable <DataTable
v-bind="config" v-bind="config"
:rows="props.data" :rows="props.data"
/> />
+30 -25
View File
@@ -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 }} -->
+113 -146
View File
@@ -1,41 +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 type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import { toast } from '~/components/pub/ui/toast'
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue' import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue' import * as CH from '~/components/pub/my-ui/content-header'
import Filter from '~/components/pub/my-ui/nav-header/filter.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { useSidebar } from '~/components/pub/ui/sidebar/utils' import { useSidebar } from '~/components/pub/ui/sidebar/utils'
// Libs // App libs
import { getPositionAs } from '~/lib/roles' import { getServicePosition } from '~/lib/roles' // previously getPositionAs
// Types
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import { ActionEvents } from '~/components/pub/my-ui/data/types'
// Services // Services
import { getList as getEncounterList, remove as removeEncounter, cancel as cancelEncounter } from '~/services/encounter.service' import { getList as getEncounterList, remove as removeEncounter, cancel as cancelEncounter } from '~/services/encounter.service'
// 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<{
classCode?: 'ambulatory' | 'emergency' | 'inpatient'
subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
canCreate?: boolean
canUpdate?: boolean
canDelete?: boolean
}>()
///// Declarations and Flows
// Sidebar automation
const { setOpen } = useSidebar() const { setOpen } = useSidebar()
setOpen(true) setOpen(true)
const { getActiveRole } = useUserStore() // Main data
const activeRole = getActiveRole()
const activePosition = ref(getPositionAs(activeRole))
const props = defineProps<{
classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient'
subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
type: string
}>()
const data = ref([]) const data = ref([])
const isLoading = reactive<DataTableLoader>({ const isLoading = reactive<DataTableLoader>({
summary: false, summary: false,
@@ -44,67 +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 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 {
@@ -215,94 +235,32 @@ function handleRemoveConfirmation() {
recItem.value = null recItem.value = null
isRecordConfirmationOpen.value = false isRecordConfirmationOpen.value = false
} }
watch(
() => recAction.value,
() => {
if (recAction.value === ActionEvents.showConfirmDelete) {
isRecordConfirmationOpen.value = true
return
}
if (recAction.value === ActionEvents.showCancel) {
isRecordCancelOpen.value = true
return
}
const basePath = getBasePath()
if (props.type === 'encounter') {
if (recAction.value === 'showDetail') {
navigateTo(`${basePath}/${recId.value}/detail`)
} else if (recAction.value === 'showEdit') {
navigateTo(`${basePath}/${recId.value}/process`)
} else if (recAction.value === 'showPrint') {
console.log('print')
} 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
}
}
},
)
watch(getActiveRole, () => {
const activeRole = getActiveRole()
activePosition.value = getPositionAs(activeRole)
})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
provide('position', activePosition)
onMounted(() => {
getPatientList()
})
</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" v-model:open="isRecordCancelOpen"
custom-title="Batalkan Kunjungan" custom-title="Batalkan Kunjungan"
custom-message="Apakah anda yakin ingin membatalkan kunjungan pasien berikut?" custom-message="Apakah anda yakin ingin membatalkan kunjungan pasien berikut?"
@@ -325,7 +283,9 @@ onMounted(() => {
</template> </template>
</RecordConfirmation> </RecordConfirmation>
<!-- Hapus -->
<RecordConfirmation <RecordConfirmation
v-if="canDelete"
v-model:open="isRecordConfirmationOpen" v-model:open="isRecordConfirmationOpen"
action="delete" action="delete"
:record="recItem" :record="recItem"
@@ -349,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,132 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
// Pubs components
import ContentSwitcher from '~/components/pub/my-ui/content-switcher/content-switcher.vue'
import { useSidebar } from '~/components/pub/ui/sidebar/utils'
// Components
import EncounterPatientInfo from '~/components/app/encounter/quick-info-full.vue'
import EncounterHistoryButtonMenu from '~/components/app/encounter/quick-shortcut.vue'
import SubMenu from '~/components/pub/my-ui/menus/submenu.vue'
// Libraries
import { getPositionAs } from '~/lib/roles'
// Models
import { genEncounter } from '~/models/encounter'
// Types
import type { EncounterProps } from '~/handlers/encounter-init.handler'
// Handlers
import { getEncounterData } from '~/handlers/encounter-process.handler'
import { getMenuItems } from "~/handlers/encounter-init.handler"
const { user, getActiveRole } = useUserStore()
const route = useRoute()
const router = useRouter()
const props = defineProps<{
classCode?: EncounterProps['classCode']
subClassCode?: EncounterProps['subClassCode']
}>()
const activeRole = getActiveRole()
const activePosition = ref(getPositionAs(activeRole))
const menus = ref([] as any)
const activeMenu = computed({
get: () => (route.query?.menu && typeof route.query.menu === 'string' ? route.query.menu : 'status'),
set: (value: string) => {
router.replace({ path: route.path, query: { menu: value } })
},
})
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const data = ref<any>(genEncounter())
const isShowPatient = computed(() => data.value && data.value?.patient?.person)
const { setOpen } = useSidebar()
setOpen(false)
if (activePosition.value === 'none') { // if user position is none, redirect to home page
router.push('/')
}
// Dummy rows for ProtocolList (matches keys expected by list-cfg.protocol)
const protocolRows = [
{
number: '1',
tanggal: new Date().toISOString().substring(0, 10),
siklus: 'I',
periode: 'Siklus I',
kehadiran: 'Hadir',
action: '',
},
{
number: '2',
tanggal: new Date().toISOString().substring(0, 10),
siklus: 'II',
periode: 'Siklus II',
kehadiran: 'Tidak Hadir',
action: '',
},
]
const paginationMeta = {
recordCount: protocolRows.length,
page: 1,
pageSize: 10,
totalPage: 1,
hasNext: false,
hasPrev: false,
}
function handleClick(type: string) {
if (type === 'draft') {
router.back()
}
}
function initMenus() {
menus.value = getMenuItems(id, props, user, {
encounter: data.value
} as any, {
protocolTheraphy: paginationMeta,
protocolChemotherapy: paginationMeta,
medicineProtocolChemotherapy: paginationMeta,
})
}
async function getData() {
data.value = await getEncounterData(id)
}
watch(getActiveRole, () => {
const activeRole = getActiveRole()
activePosition.value = getPositionAs(activeRole)
initMenus()
})
onMounted(async () => {
await getData()
initMenus()
})
</script>
<template>
<div class="w-full">
<div class="mb-4">
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" @click="handleClick" />
</div>
<ContentSwitcher :active="1" :height="200">
<template v-slot:content1>
<EncounterPatientInfo v-if="isShowPatient" :data="data" />
</template>
<template v-slot:content2>
<EncounterHistoryButtonMenu v-if="isShowPatient" />
</template>
</ContentSwitcher>
<SubMenu :data="menus" :initial-active-menu="activeMenu" @change-menu="activeMenu = $event" />
</div>
</template>
+113 -85
View File
@@ -1,114 +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 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'
///// 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
}) router.push('/')
if (res.body?.data) data.value = res.body?.data
} }
onMounted(() => { // Dummy rows for ProtocolList (matches keys expected by list-cfg.protocol)
fetchDetail() const protocolRows = [
})
const tabs: TabItem[] = [
{ value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } },
{ {
value: 'early-medical-assessment', number: '1',
label: 'Pengkajian Awal Medis', tanggal: new Date().toISOString().substring(0, 10),
component: EarlyMedicalAssesmentList, siklus: 'I',
props: { encounter: data, type: 'early-medic', label: 'Pengkajian Awal Medis' }, periode: 'Siklus I',
kehadiran: 'Hadir',
action: '',
}, },
{ {
value: 'rehab-medical-assessment', number: '2',
label: 'Pengkajian Awal Medis Rehabilitasi Medis', tanggal: new Date().toISOString().substring(0, 10),
component: EarlyMedicalRehabList, siklus: 'II',
props: { encounter: data, type: 'early-rehab', label: 'Pengkajian Awal Medis Rehabilitasi Medis' }, periode: 'Siklus II',
}, kehadiran: 'Tidak Hadir',
{ action: '',
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 },
}, },
] ]
const paginationMeta = {
recordCount: protocolRows.length,
page: 1,
pageSize: 10,
totalPage: 1,
hasNext: false,
hasPrev: false,
}
// Reacrtivities
watch(getActiveRole, () => {
activePosition.value = getServicePosition(userActiveRole)
initMenus()
})
onMounted(async () => {
await getData()
initMenus()
})
///// Functions
function handleClick(type: string) {
if (type === 'draft') {
router.back()
}
}
function initMenus() {
menus.value = getMenuItems(id, props, user, {
encounter: data.value
} as any, {
protocolTheraphy: paginationMeta,
protocolChemotherapy: paginationMeta,
medicineProtocolChemotherapy: paginationMeta,
})
}
async function getData() {
data.value = await getEncounterData(id)
}
</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>
+10 -1
View File
@@ -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()
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ const props = defineProps<{
height: 200 height?: number
activeTab?: 1 | 2 activeTab?: 1 | 2
}>() }>()
@@ -12,7 +12,7 @@ function handleClick(value: 1 | 2) {
</script> </script>
<template> <template>
<div class="content-switcher" :style="`height: ${height}px`"> <div class="content-switcher" :style="`height: ${height || 200}px`">
<div :class="`${activeTab === 1 ? 'active' : 'inactive'}`"> <div :class="`${activeTab === 1 ? 'active' : 'inactive'}`">
<div class="content-wrapper"> <div class="content-wrapper">
<div> <div>
@@ -49,7 +49,7 @@ function handleClick(value: 1 | 2) {
} }
.content-wrapper { .content-wrapper {
@apply p-4 2xl:p-5 overflow-hidden @apply p-4 2xl:p-5 overflow-hidden grow
} }
.inactive .content-wrapper { .inactive .content-wrapper {
@apply p-0 w-0 @apply p-0 w-0
+4 -2
View File
@@ -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>
+16 -10
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { type EncounterItem } from "../../../../handlers/encounter-process.handler"; import { type EncounterItem } from "~/handlers/encounter-init.handler";
const props = defineProps<{ const props = defineProps<{
initialActiveMenu: string initialActiveMenu: string
@@ -18,22 +18,28 @@ function changeMenu(value: string) {
</script> </script>
<template> <template>
<div class="mt-4 flex gap-4"> <div class="flex">
<!-- Menu Sidebar --> <!-- 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 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 p-2"> <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" <button
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" 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)"> @click="changeMenu(menu.id)">
{{ menu.title }} {{ menu.title }}
</button> </button>
</div> </div>
</div> </div>
<!-- Active Menu Content --> <!-- Active Menu Content -->
<div v-if="data.find((m) => m.id === activeMenu)?.component" <div class="p-4 2xl:p-5 flex-grow">
class="flex-1 rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950"> <div v-if="data.find((m) => m.id === activeMenu)?.component"
<component :is="data.find((m) => m.id === activeMenu)?.component" class="flex-1 rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
v-bind="data.find((m) => m.id === activeMenu)?.props" /> <component
:is="data.find((m) => m.id === activeMenu)?.component"
v-bind="data.find((m) => m.id === activeMenu)?.props" />
</div>
</div> </div>
</div> </div>
</template> </template>
+1 -1
View File
@@ -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
View File
@@ -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 === 'system' || (role in roleAccess)) // 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) => role === 'system' || roleAccess[role]?.includes(permission)) // 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),
+36
View File
@@ -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}`
}
@@ -4,13 +4,13 @@ import type { Permission } from "~/models/role";
// export type Keys = 'key1' | 'key2' | 'key3' | etc // export type Keys = 'key1' | 'key2' | 'key3' | etc
export const permissions: Record<string, Record<string, Permission[]>> = { export const permissions: Record<string, Record<string, Permission[]>> = {
'/outpatient/registration-queue': { '/ambulatory/registration-queue': {
'emp|reg': ['R', 'U', 'D'], 'emp|reg': ['R', 'U', 'D'],
}, },
'/outpatient/encounter-queue': { '/ambulatory/encounter-queue': {
'emp|nur': ['R', 'U', 'D'], 'emp|nur': ['R', 'U', 'D'],
}, },
'/outpatient/encounter': { '/ambulatory/encounter': {
'emp|reg': ['C', 'R', 'U', 'D'], 'emp|reg': ['C', 'R', 'U', 'D'],
'emp|doc': ['R'], 'emp|doc': ['R'],
'emp|nur': ['R'], 'emp|nur': ['R'],
@@ -21,10 +21,10 @@ export const permissions: Record<string, Record<string, Permission[]>> = {
'emp|lab': ['R'], 'emp|lab': ['R'],
'emp|rad': ['R'], 'emp|rad': ['R'],
}, },
'/outpatient/encounter/add': { '/ambulatory/encounter/add': {
'emp|reg': ['C', 'R', 'U', 'D'], 'emp|reg': ['C', 'R', 'U', 'D'],
}, },
'/outpatient/encounter/[id]/detail': { '/ambulatory/encounter/[id]': {
'emp|reg': ['C', 'R', 'U', 'D'], 'emp|reg': ['C', 'R', 'U', 'D'],
'emp|doc': ['R'], 'emp|doc': ['R'],
'emp|nur': ['R'], 'emp|nur': ['R'],
@@ -35,10 +35,10 @@ export const permissions: Record<string, Record<string, Permission[]>> = {
'emp|lab': ['R'], 'emp|lab': ['R'],
'emp|rad': ['R'], 'emp|rad': ['R'],
}, },
'/outpatient/encounter/[id]/edit': { '/ambulatory/encounter/[id]/edit': {
'emp|reg': ['C', 'R', 'U', 'D'], 'emp|reg': ['C', 'R', 'U', 'D'],
}, },
'/outpatient/encounter/[id]/process': { '/ambulatory/encounter/[id]/process': {
'emp|doc': ['R', 'U'], 'emp|doc': ['R', 'U'],
'emp|nur': ['R', 'U'], 'emp|nur': ['R', 'U'],
'emp|thr': ['R', 'U'], 'emp|thr': ['R', 'U'],
@@ -48,10 +48,10 @@ export const permissions: Record<string, Record<string, Permission[]>> = {
'emp|lab': ['R', 'U'], 'emp|lab': ['R', 'U'],
'emp|rad': ['R', 'U'], 'emp|rad': ['R', 'U'],
}, },
'/outpatient/consulation': { '/ambulatory/consulation': {
'emp|doc': ['R'], 'emp|doc': ['R'],
}, },
'/outpatient/consulation/[id]/process': { '/ambulatory/consulation/[id]/process': {
'emp|doc': ['R', 'U'], 'emp|doc': ['R', 'U'],
}, },
} }
+16
View File
@@ -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'],
},
}
+7 -5
View File
@@ -1,5 +1,5 @@
import { isValidDate } from '~/lib/date' import { isValidDate } from '~/lib/date'
import { medicalPositions } from '~/lib/roles' import { medicalRoles } from '~/const/common/role'
export interface EncounterItem { export interface EncounterItem {
id: string id: string
@@ -444,7 +444,9 @@ export function getMenuItems(
data: EncounterListData, data: EncounterListData,
meta: any, meta: any,
) { ) {
const normalClassCode = props.classCode === 'ambulatory' ? 'outpatient' : props.classCode 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 currentKeys = injectComponents(id, data, meta)
const defaultItems: EncounterItem[] = Object.values(currentKeys) const defaultItems: EncounterItem[] = Object.values(currentKeys)
const listItemsForOutpatientRehab = mergeArrayAt( const listItemsForOutpatientRehab = mergeArrayAt(
@@ -456,14 +458,14 @@ export function getMenuItems(
getItemsAll('ambulatory', 'chemo', defaultItems), getItemsAll('ambulatory', 'chemo', defaultItems),
) )
const listItems: Record<string, Record<string, Record<string, any>>> = { const listItems: Record<string, Record<string, Record<string, any>>> = {
'installation|outpatient': { 'installation|ambulatory': {
'unit|rehab': { 'unit|rehab': {
items: listItemsForOutpatientRehab, items: listItemsForOutpatientRehab,
roles: medicalPositions, roles: medicalRoles,
}, },
'unit|chemo': { 'unit|chemo': {
items: listItemsForOutpatientChemo, items: listItemsForOutpatientChemo,
roles: medicalPositions, roles: medicalRoles,
}, },
all: getItemsAll('ambulatory', 'all', defaultItems), all: getItemsAll('ambulatory', 'all', defaultItems),
}, },
+8 -2
View File
@@ -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>
+13 -11
View File
@@ -1,14 +1,16 @@
export const medicalPositions = ['emp|doc', 'emp|lab', 'emp|mid', 'emp|nur', 'emp|nut', 'emp|pha', 'emp|reg'] import { medicalRoles, respPosCode } from '~/const/common/role'
const verificatorRole = 'verificator'
export function getPositionAs(roleAccess: string): string { export function getServicePosition(role?: string): string {
if (roleAccess.includes('|')) { if(!role) {
if (medicalPositions.includes(roleAccess)) { return 'none'
return 'medical' }
} if (medicalRoles.includes(role)) {
if (roleAccess.includes(verificatorRole)) { return 'medical'
return 'verificator' } else if (role === 'emp|reg') {
} return 'registration'
} else if (role.includes('|resp')) {
return 'verificator'
} else {
return 'none'
} }
return 'none'
} }
+1 -1
View File
@@ -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))
+3 -3
View File
@@ -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]
@@ -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>
@@ -0,0 +1,47 @@
<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'
import { medicalRoles } from '~/const/common/role'
// Page meta
definePageMeta({
middleware: ['rbac'],
roles: medicalRoles,
title: 'Detail Kunjungan',
contentFrame: 'cf-container-md',
})
// Define common things
const route = useRoute()
// Prep role checking
const roleAccess: Record<string, Permission[]> = permissions['/ambulatory/encounter/[id]'] || {}
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
useHead({
title: () => route.meta.title as string,
})
</script>
<template>
<div>
<div v-if="canRead">
<ContentOutpatientEncounterDetail :patient-id="Number(route.params.id)" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -1,18 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Permission } from '~/models/role' // Pubs
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 Content from '~/components/content/encounter/process-next.vue'
// AppS
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/ambulatory'
import Content from '~/components/content/encounter/process.vue'
definePageMeta({ definePageMeta({
middleware: ['rbac'], middleware: ['rbac'],
roles: ['emp|doc', 'emp|nur', 'emp|reg', 'emp|pha', 'emp|pay', 'emp|mng'], roles: ['emp|doc', 'emp|nur', 'emp|miw', 'emp|nut', 'emp|lab', 'emp|pha', 'emp|thr'],
title: 'Tambah Kunjungan', title: 'Proses Kunjungan',
contentFrame: 'cf-full-width', contentFrame: 'cf-full-width',
contentPadding: 'p-0',
contentUseCard: false
}) })
// Preps role checking // Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {} const roleAccess: Record<string, Permission[]> = permissions['/ambulatory/encounter/[id]/process'] || {}
const { checkRole, hasReadAccess } = useRBAC() const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page // Check if user has access to this page
@@ -29,11 +34,12 @@ const route = useRoute()
useHead({ useHead({
title: () => `${route.meta.title}`, title: () => `${route.meta.title}`,
}) })
</script> </script>
<template> <template>
<div v-if="canRead"> <div v-if="canRead">
<Content class-code="ambulatory" sub-class-code="reg" /> <Content class-code="ambulatory" />
</div> </div>
<Error v-else :status-code="403" /> <Error v-else :status-code="403" />
</template> </template>
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Permission } from '~/models/role' import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient' 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 Content from '~/components/content/encounter/entry.vue' import Content from '~/components/content/encounter/entry.vue'
@@ -13,7 +13,7 @@ definePageMeta({
}) })
// Preps role checking // Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {} const roleAccess: Record<string, Permission[]> = permissions['/ambulatory/encounter/add'] || {}
const { checkRole, hasReadAccess, hasCreateAccess } = useRBAC() const { checkRole, hasReadAccess, hasCreateAccess } = useRBAC()
// Check if user has access to this page // Check if user has access to this page
@@ -1,43 +1,45 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Permission } from '~/models/role' // Pubs
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'
// Models & Consts
import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/ambulatory'
// Apps
import Content from '~/components/content/encounter/list.vue' import Content from '~/components/content/encounter/list.vue'
// Page meta
definePageMeta({ definePageMeta({
middleware: ['rbac'], // middleware: ['rbac'],
roles: ['emp|reg', 'emp|nur', 'emp|doc', 'emp|miw', 'emp|thr', 'emp|nut', 'emp|pha', 'emp|lab'], // 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',
}) })
// Define common things
const route = useRoute()
// Preps role checking // Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {} const roleAccess: Record<string, Permission[]> = permissions[route.path] || {}
const { checkRole, hasReadAccess } = useRBAC() const { checkRole, hasCreateAccess, 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) { const canRead = hasReadAccess(roleAccess)
if (!hasAccess || !canRead) {
navigateTo('/403') navigateTo('/403')
} }
const canCreate = hasCreateAccess(roleAccess)
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
// Page needs // Page needs
const route = useRoute()
useHead({ useHead({
title: () => route.meta.title as string, title: () => route.meta.title as string,
}) })
const { user, getActiveRole } = useUserStore() // User info
// const activeRole = getActiveRole() const { user } = useUserStore()
// const activeRoleParts = activeRole ? activeRole.split('|') : ['', '']
// const activeRolePos = activeRoleParts[0] // reaching this page means it is already set
// const activeRoleType = activeRoleParts[1] == 'rehab' ? activeRoleParts[1] : ''
const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg' const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg'
</script> </script>
<template> <template>
@@ -46,7 +48,7 @@ const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg'
<Content <Content
class-code="ambulatory" class-code="ambulatory"
:sub-class-code="subClassCode" :sub-class-code="subClassCode"
type="encounter" :can-create="canCreate"
/> />
</div> </div>
<Error <Error
@@ -0,0 +1,47 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import ContentChemotherapyAdminList from '~/components/content/chemotherapy/admin-list.vue'
import ContentChemotherapyVerification from '~/components/content/chemotherapy/verification.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Kemoterapi Admin',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => 'Verifikasi Jadwal Pasien',
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = true // hasReadAccess(roleAccess)
const mode = computed(() => route.params.mode as string)
</script>
<template>
<div>
<div v-if="canRead">
<ContentChemotherapyVerification />
</div>
<Error
v-else
:status-code="403"
/>
</div>
</template>
@@ -0,0 +1,55 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import ContentChemotherapyAdminList from '~/components/content/chemotherapy/admin-list.vue'
import ContentChemotherapyVerification from '~/components/content/chemotherapy/verification.vue'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Kemoterapi Admin',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => {
const mode = route.params.mode as string
if (mode === 'admin') {
return 'Administrasi Pasien Rawat Jalan Kemoterapi'
} else if (mode === 'series') {
return 'Proses Jadwal Pasien'
}
return route.meta.title as string
},
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor'] || {}
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = true // hasReadAccess(roleAccess)
const mode = computed(() => route.params.mode as string)
</script>
<template>
<div>
<div v-if="canRead">
<ContentChemotherapyAdminList v-if="mode === 'admin'" />
<ContentChemotherapyProcess v-else-if="mode === 'series'" />
<Error v-else :status-code="404" status-message="Mode tidak ditemukan" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -0,0 +1,9 @@
<script setup lang="ts">
// Redirect to list page - the process page is now at [id]/index.vue
const route = useRoute()
navigateTo('/outpation-action/chemotherapy/list')
</script>
<template>
<div />
</template>
@@ -1,13 +1,13 @@
<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'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'], roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Detail Surat Kontrol', title: 'Daftar Kempterapi',
contentFrame: 'cf-container-md', contentFrame: 'cf-full-width',
}) })
const route = useRoute() const route = useRoute()
@@ -16,25 +16,24 @@ useHead({
title: () => route.meta.title as string, title: () => route.meta.title as string,
}) })
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient'] const roleAccess: Record<string, Permission[]> = permissions['/chemotherapy'] || {}
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
</script> </script>
<template> <template>
<div> <div>
<div v-if="canRead"> <div v-if="canRead">
<ContentOutpatientEncounterDetail :patient-id="Number(route.params.id)" /> <ContentChemotherapyList />
</div> </div>
<Error v-else :status-code="403" /> <Error v-else :status-code="403" />
</div> </div>
@@ -0,0 +1,10 @@
<script setup lang="ts">
const route = useRoute();
</script>
<template>
<div class="p-10 text-center">
<div class="mb-5 text-base font-semibold">Hello world!!</div>
<div>You are accessing "{{ route.fullPath }}"</div>
</div>
</template>
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Permission } from '~/models/role' import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient' 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 Content from '~/components/content/encounter/entry.vue' import Content from '~/components/content/encounter/entry.vue'
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Permission } from '~/models/role' import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient' 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 Content from '~/components/content/encounter/list.vue' import Content from '~/components/content/encounter/list.vue'
@@ -13,7 +13,8 @@ definePageMeta({
}) })
// Preps role checking // Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {} const route = useRoute()
const roleAccess: Record<string, Permission[]> = permissions[route.path] || {}
const { checkRole, hasReadAccess } = useRBAC() const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page // Check if user has access to this page
@@ -26,18 +27,12 @@ if (!hasAccess) {
const canRead = hasReadAccess(roleAccess) const canRead = hasReadAccess(roleAccess)
// Page needs // Page needs
const route = useRoute()
useHead({ useHead({
title: () => route.meta.title as string, title: () => route.meta.title as string,
}) })
const { user, getActiveRole } = useUserStore() const { user } = useUserStore()
// const activeRole = getActiveRole()
// const activeRoleParts = activeRole ? activeRole.split('|') : ['', '']
// const activeRolePos = activeRoleParts[0] // reaching this page means it is already set
// const activeRoleType = activeRoleParts[1] == 'rehab' ? activeRoleParts[1] : ''
const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg' const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg'
</script> </script>
<template> <template>
@@ -45,7 +40,7 @@ const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg'
<div v-if="canRead"> <div v-if="canRead">
<Content <Content
class-code="emergency" class-code="emergency"
sub-class-code="reg" :sub-class-code="subClassCode"
type="encounter" type="encounter"
/> />
</div> </div>
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
const route = useRoute();
</script>
<template>
<div class="p-10 text-center">
<div class="mb-5 text-base font-semibold">Hello world!!</div>
<div>You are accessing "{{ route.fullPath }}"</div>
</div>
</template>
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Permission } from '~/models/role' import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient' 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 Content from '~/components/content/encounter/entry.vue' import Content from '~/components/content/encounter/entry.vue'
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Permission } from '~/models/role' import type { Permission } from '~/models/role'
import { permissions } from '~/const/page-permission/outpatient' import { permissions } from '~/const/page-permission/inpatient'
import Error from '~/components/pub/my-ui/error/error.vue' import Error from '~/components/pub/my-ui/error/error.vue'
import Content from '~/components/content/encounter/list.vue' import Content from '~/components/content/encounter/list.vue'
@@ -13,7 +13,7 @@ definePageMeta({
}) })
// Preps role checking // Preps role checking
const roleAccess: Record<string, Permission[]> = permissions['/outpatient/encounter'] || {} const roleAccess: Record<string, Permission[]> = permissions['/inpatient/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
@@ -31,11 +31,7 @@ useHead({
title: () => route.meta.title as string, title: () => route.meta.title as string,
}) })
const { user, getActiveRole } = useUserStore() const { user } = useUserStore()
// const activeRole = getActiveRole()
// const activeRoleParts = activeRole ? activeRole.split('|') : ['', '']
// const activeRolePos = activeRoleParts[0] // reaching this page means it is already set
// const activeRoleType = activeRoleParts[1] == 'rehab' ? activeRoleParts[1] : ''
const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg' const subClassCode = user.unit_code == 'rehab' ? 'rehab' : 'reg'
</script> </script>
@@ -1,5 +0,0 @@
<script setup lang="ts"></script>
<template>
<FlowRehabRegistrationHome />
</template>
@@ -1,40 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah SEP Prosedur',
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>
<PubMyUiError v-else :status-code="403" />
</template>
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
const route = useRoute();
</script>
<template>
<div class="p-10 text-center">
<div class="mb-5 text-base font-semibold">Hello world!!</div>
<div>You are accessing "{{ route.fullPath }}"</div>
</div>
</template>
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
const route = useRoute();
</script>
<template>
<div class="p-10 text-center">
<div class="mb-5 text-base font-semibold">Hello world!!</div>
<div>You are accessing "{{ route.fullPath }}"</div>
</div>
</template>
@@ -5,7 +5,7 @@ import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({ definePageMeta({
middleware: ['rbac'], middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'], roles: [],
title: 'Daftar Dokter', title: 'Daftar Dokter',
contentFrame: 'cf-full-width', contentFrame: 'cf-full-width',
}) })
@@ -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: PagePermission = PAGE_PERMISSIONS['/tools-equipment-src/medicine']!
const { checkRole, hasReadAccess } = useRBAC() const { checkRole, hasReadAccess } = useRBAC()
+12 -7
View File
@@ -1,19 +1,21 @@
export const useUserStore = defineStore( export const useUserStore = defineStore(
'user', 'user',
() => { () => {
// should be auth type
const user = ref<any | null>(null) const user = ref<any | null>(null)
// const token = useCookie('authentication') // const token = useCookie('authentication')
const isAuthenticated = computed(() => !!user.value) const isAuthenticated = computed(() => !!user.value)
const userRole = computed(() => { const userRoles = computed(() => {
return user.value?.roles || [] return user.value?.roles || []
// return roles.map((input: string) => {
// const parts = input.split('|')
// return parts.length > 1 ? parts[1]: parts[0]
// })
}) })
const userActiveRole = ref('') // watched user failed, so create a ref here
// computed(() => {
// // return user.value?.activeRole || ''
// })
const login = async (userData: any) => { const login = async (userData: any) => {
user.value = userData user.value = userData
} }
@@ -25,10 +27,11 @@ export const useUserStore = defineStore(
const setActiveRole = (role: string) => { const setActiveRole = (role: string) => {
if (user.value && user.value.roles.includes(role)) { if (user.value && user.value.roles.includes(role)) {
user.value.activeRole = role user.value.activeRole = role
userActiveRole.value = role
} }
} }
const getActiveRole = () => { const getActiveRole = (): string | undefined => {
if (user.value?.activeRole) { if (user.value?.activeRole) {
return user.value.activeRole return user.value.activeRole
} }
@@ -36,12 +39,14 @@ export const useUserStore = defineStore(
user.value.activeRole = user.value.roles[0] user.value.activeRole = user.value.roles[0]
return user.value.activeRole return user.value.activeRole
} }
return undefined
} }
return { return {
user, user,
isAuthenticated, isAuthenticated,
userRole, userRoles,
userActiveRole,
login, login,
logout, logout,
setActiveRole, setActiveRole,
+2 -2
View File
@@ -14,12 +14,12 @@
{ {
"title": "Kunjungan", "title": "Kunjungan",
"icon": "i-lucide-stethoscope", "icon": "i-lucide-stethoscope",
"link": "/outpatient/encounter" "link": "/ambulatory/encounter"
}, },
{ {
"title": "Konsultasi", "title": "Konsultasi",
"icon": "i-lucide-building-2", "icon": "i-lucide-building-2",
"link": "/outpatient/consultation" "link": "/ambulatory/consultation"
} }
] ]
}, },
+1 -1
View File
@@ -10,7 +10,7 @@
{ {
"title": "Rawat Jalan", "title": "Rawat Jalan",
"icon": "i-lucide-stethoscope", "icon": "i-lucide-stethoscope",
"link": "/outpatient/encounter" "link": "/ambulatory/encounter"
}, },
{ {
"title": "IGD", "title": "IGD",
+2 -2
View File
@@ -13,11 +13,11 @@
"children": [ "children": [
{ {
"title": "Antrian Poliklinik", "title": "Antrian Poliklinik",
"link": "/outpatient/encounter-queue" "link": "/ambulatory/encounter-queue"
}, },
{ {
"title": "Kunjungan", "title": "Kunjungan",
"link": "/outpatient/encounter" "link": "/ambulatory/encounter"
} }
] ]
}, },
+1 -1
View File
@@ -10,7 +10,7 @@
{ {
"title": "Rawat Jalan", "title": "Rawat Jalan",
"icon": "i-lucide-stethoscope", "icon": "i-lucide-stethoscope",
"link": "/outpatient/encounter" "link": "/ambulatory/encounter"
}, },
{ {
"title": "IGD", "title": "IGD",
+2 -2
View File
@@ -13,11 +13,11 @@
"children": [ "children": [
{ {
"title": "Antrian Pendaftaran", "title": "Antrian Pendaftaran",
"link": "/outpatient/registration-queue" "link": "/ambulatory/registration-queue"
}, },
{ {
"title": "Kunjungan", "title": "Kunjungan",
"link": "/outpatient/encounter" "link": "/ambulatory/encounter"
} }
] ]
}, },
+4 -4
View File
@@ -13,19 +13,19 @@
"children": [ "children": [
{ {
"title": "Antrian Pendaftaran", "title": "Antrian Pendaftaran",
"link": "/outpatient/registration-queue" "link": "/ambulatory/registration-queue"
}, },
{ {
"title": "Antrian Poliklinik", "title": "Antrian Poliklinik",
"link": "/outpatient/encounter-queue" "link": "/ambulatory/encounter-queue"
}, },
{ {
"title": "Kunjungan", "title": "Kunjungan",
"link": "/outpatient/encounter" "link": "/ambulatory/encounter"
}, },
{ {
"title": "Konsultasi", "title": "Konsultasi",
"link": "/outpatient/consultation" "link": "/ambulatory/consultation"
} }
] ]
}, },
+5 -5
View File
@@ -13,19 +13,19 @@
"children": [ "children": [
{ {
"title": "Antrian Pendaftaran", "title": "Antrian Pendaftaran",
"link": "/outpatient/registration-queue" "link": "/ambulatory/registration-queue"
}, },
{ {
"title": "Antrian Poliklinik", "title": "Antrian Poliklinik",
"link": "/outpatient/encounter-queue" "link": "/ambulatory/encounter-queue"
}, },
{ {
"title": "Kunjungan", "title": "Kunjungan",
"link": "/outpatient/encounter" "link": "/ambulatory/encounter"
}, },
{ {
"title": "Konsultasi", "title": "Konsultasi",
"link": "/outpatient/consultation" "link": "/ambulatory/consultation"
} }
] ]
}, },
@@ -389,4 +389,4 @@
} }
] ]
} }
] ]