Feat: add UI BPJS > Surat Kontrol
This commit is contained in:
No files matched your search
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ActionEvents, type LinkItem, type ListItemDto } from '~/components/pub/my-ui/data/types';
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rec: ListItemDto
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const recId = inject<Ref<number>>('rec_id')!
|
||||||
|
const recAction = inject<Ref<string>>('rec_action')!
|
||||||
|
const recItem = inject<Ref<any>>('rec_item')!
|
||||||
|
const activeKey = ref<string | null>(null)
|
||||||
|
const linkItems: LinkItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Print',
|
||||||
|
onClick: () => {
|
||||||
|
print()
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-printer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Log History',
|
||||||
|
onClick: () => {
|
||||||
|
history()
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-logs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hapus',
|
||||||
|
onClick: () => {
|
||||||
|
del()
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-trash',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function print() {
|
||||||
|
recId.value = props.rec.id || 0
|
||||||
|
recAction.value = ActionEvents.showProcess
|
||||||
|
recItem.value = props.rec
|
||||||
|
}
|
||||||
|
|
||||||
|
function history() {
|
||||||
|
recId.value = props.rec.id || 0
|
||||||
|
recAction.value = ActionEvents.showDetail
|
||||||
|
recItem.value = props.rec
|
||||||
|
}
|
||||||
|
|
||||||
|
function del() {
|
||||||
|
recId.value = props.rec.id || 0
|
||||||
|
recAction.value = ActionEvents.showConfirmDelete
|
||||||
|
recItem.value = props.rec
|
||||||
|
}
|
||||||
|
</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 linkItems"
|
||||||
|
: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>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||||
|
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||||
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
|
import { Form } from '~/components/pub/ui/form'
|
||||||
|
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||||
|
import SelectOriginPolyclinic from '~/components/app/bpjs/control-letter/_common/select-origin-polyclinic.vue'
|
||||||
|
import SelectDestinationPolyclinic from '~/components/app/bpjs/control-letter/_common/select-destination-polyclinic.vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps()
|
||||||
|
|
||||||
|
const items = reactive([
|
||||||
|
{ id: 1, description: 'Shipped from warehouse', createdAt: new Date(Date.now() - 86400000 * 2) },
|
||||||
|
{ id: 2, description: 'In transit to distribution center', createdAt: new Date(Date.now() - 86400000) },
|
||||||
|
{ id: 3, description: 'Out for delivery (Current)', createdAt: new Date() },
|
||||||
|
])
|
||||||
|
const itemsCount = computed(() => items.length || 0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul :class="cn('pb-5 flex flex-col min-h-[30rem]', '')">
|
||||||
|
<li v-for="(item, index) in items" :key="item.id" class="flex gap-3 items-start">
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div class="h-5 w-5 rounded-full border-2 border-gray-300 flex items-center justify-center">
|
||||||
|
<div :class="cn('dark:bg-white border-gray-300 rounded-full p-1.5',
|
||||||
|
index === 0 ? 'bg-green-500' : 'bg-transparent'
|
||||||
|
)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr v-if="index !== itemsCount - 1" class="h-8 w-0.5 bg-gray-300 dark:bg-gray-300" aria-hidden="true">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center min-w-96">
|
||||||
|
<div class="max-w-80">
|
||||||
|
<time :class="cn('font-medium text-gray-800 dark:text-gray-100', '')">
|
||||||
|
{{ item?.createdAt.toLocaleDateString('id-ID') }}
|
||||||
|
</time>
|
||||||
|
<h1 :class="cn('text-gray-500 dark:text-gray-400', '')">{{ item.description }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
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 * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const dateRange = ref<{ from: Date | null; to: Date | null }>({
|
||||||
|
from: new Date(),
|
||||||
|
to: new Date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const df = new DateFormatter('en-US', {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
})
|
||||||
|
|
||||||
|
const value = ref({
|
||||||
|
start: new CalendarDate(2022, 1, 20),
|
||||||
|
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
|
||||||
|
}) as Ref<DateRange>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
:class="cn('w-full bg-white border-gray-400 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>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||||
|
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||||
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
|
import { Form } from '~/components/pub/ui/form'
|
||||||
|
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||||
|
import SelectOriginPolyclinic from '~/components/app/bpjs/control-letter/_common/select-origin-polyclinic.vue'
|
||||||
|
import SelectDestinationPolyclinic from '~/components/app/bpjs/control-letter/_common/select-destination-polyclinic.vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import SelectDateRange from './_common/select-date-range.vue'
|
||||||
|
|
||||||
|
interface InstallationFormData {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
encounterClassCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
installation: {
|
||||||
|
msg: {
|
||||||
|
placeholder: string
|
||||||
|
}
|
||||||
|
items: {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
code: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
schema: any
|
||||||
|
initialValues?: Partial<InstallationFormData>
|
||||||
|
errors?: FormErrors
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [values: InstallationFormData, resetForm: () => void]
|
||||||
|
reset: [resetForm: () => void]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formSchema = toTypedSchema(props.schema)
|
||||||
|
|
||||||
|
// Form submission handler
|
||||||
|
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
|
||||||
|
const formData: InstallationFormData = {
|
||||||
|
name: values.name || '',
|
||||||
|
code: values.code || '',
|
||||||
|
encounterClassCode: values.encounterClassCode || '',
|
||||||
|
}
|
||||||
|
emit('submit', formData, resetForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form cancel handler
|
||||||
|
function onResetForm({ resetForm }: { resetForm: () => void }) {
|
||||||
|
emit('reset', resetForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = ref([
|
||||||
|
{ label: 'Rujukan Internal', value: 'ri' },
|
||||||
|
{ label: 'SEP Rujukan', value: 'sr' },
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
v-slot="{ handleSubmit, resetForm }"
|
||||||
|
as=""
|
||||||
|
keep-values
|
||||||
|
:validation-schema="formSchema"
|
||||||
|
:initial-values="initialValues"
|
||||||
|
>
|
||||||
|
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
|
||||||
|
<div class="mb-5 border-b border-b-slate-300 pb-7 text-lg xl:text-xl">
|
||||||
|
<div class="flex flex-col justify-between">
|
||||||
|
<SelectDateRange
|
||||||
|
field-name="releaseDate"
|
||||||
|
label="Tanggal Penerbitan"
|
||||||
|
placeholder="Tanggal Penerbitan"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectDateRange
|
||||||
|
field-name="controlPlanDate"
|
||||||
|
label="Tanggal Rencana Kontrol"
|
||||||
|
placeholder="Tanggal Rencana Kontrol"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="patientName"
|
||||||
|
label="Nama Pasien"
|
||||||
|
placeholder="Nama Pasien"
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="cardNumber"
|
||||||
|
label="Nomor Kartu"
|
||||||
|
placeholder="Nomor Kartu"
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="sepNumber"
|
||||||
|
label="Nomor SEP"
|
||||||
|
placeholder="Nomor SEP"
|
||||||
|
/>
|
||||||
|
<SelectOriginPolyclinic
|
||||||
|
field-name="originPolyclinic"
|
||||||
|
label="Poliklinik Asal"
|
||||||
|
placeholder="Pilih Poliklinik Asal"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectDestinationPolyclinic
|
||||||
|
field-name="destinationPolyclinic"
|
||||||
|
label="Poliklinik Tujuan"
|
||||||
|
placeholder="Pilih Poliklinik Tujuan"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-2 flex items-center gap-3 justify-end">
|
||||||
|
<Button @click="onResetForm" variant="secondary">Reset</Button>
|
||||||
|
<Button @click="onSubmitForm">Terapkan</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||||
|
import type { Patient } from '~/models/patient'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
import { educationCodes, genderCodes } from '~/lib/constants'
|
||||||
|
import { calculateAge } from '~/lib/utils'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('./_common/dropdown-action.vue'))
|
||||||
|
const statusBadge = defineAsyncComponent(() => import('~/components/pub/my-ui/badge/status-badge.vue'))
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{}, {}, {}, {},{}, {}, {}, {}, {}, {width: 90},{width: 10},],
|
||||||
|
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'No Surat' },
|
||||||
|
{ label: 'No MR' },
|
||||||
|
{ label: 'Nama' },
|
||||||
|
{ label: 'Tgl Rencana Kontrol' },
|
||||||
|
{ label: 'Tgl Penerbitan' },
|
||||||
|
{ label: 'Klinik Asal' },
|
||||||
|
{ label: 'Klinik Tujuan' },
|
||||||
|
{ label: 'DPJP' },
|
||||||
|
{ label: 'No SEP Asal' },
|
||||||
|
{ label: 'Status' },
|
||||||
|
{ label: 'Action' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keys: ['birth_date', 'number', 'person.name', 'birth_date', 'birth_date',
|
||||||
|
'birth_date', 'number', 'person.name', 'birth_date', 'status', 'action'],
|
||||||
|
|
||||||
|
delKeyNames: [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
],
|
||||||
|
|
||||||
|
parses: {
|
||||||
|
patientId: (rec: unknown): unknown => {
|
||||||
|
const patient = rec as Patient
|
||||||
|
return patient.number
|
||||||
|
},
|
||||||
|
identity_number: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (person.nationality == 'WNA') {
|
||||||
|
return person.passportNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
return person.residentIdentityNumber || '-'
|
||||||
|
},
|
||||||
|
birth_date: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.birthDate == 'object' && person.birthDate) {
|
||||||
|
return (person.birthDate as Date).toLocaleDateString('id-ID')
|
||||||
|
} else if (typeof person.birthDate == 'string') {
|
||||||
|
return (person.birthDate as string).substring(0, 10)
|
||||||
|
}
|
||||||
|
return person.birthDate
|
||||||
|
},
|
||||||
|
patient_age: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
return calculateAge(person.birthDate)
|
||||||
|
},
|
||||||
|
gender: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.gender_code == 'number' && person.gender_code >= 0) {
|
||||||
|
return person.gender_code
|
||||||
|
} else if (typeof person.gender_code === 'string' && person.gender_code) {
|
||||||
|
return genderCodes[person.gender_code] || '-'
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
education: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
if (typeof person.education_code == 'number' && person.education_code >= 0) {
|
||||||
|
return person.education_code
|
||||||
|
} else if (typeof person.education_code === 'string' && person.education_code) {
|
||||||
|
return educationCodes[person.education_code] || '-'
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
return {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status(rec, idx) {
|
||||||
|
return {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: statusBadge,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<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
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HeaderPrep, RefExportNav, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||||
|
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
|
||||||
|
|
||||||
|
// #region Imports
|
||||||
|
import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
|
||||||
|
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||||
|
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||||
|
|
||||||
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
import Filter from '~/components/pub/my-ui/nav-header/filter.vue'
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import SummaryCard from '~/components/pub/my-ui/summary-card/summary-card.vue'
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
|
||||||
|
import { getPatients, removePatient } from '~/services/patient.service'
|
||||||
|
import FilterDialog from '~/components/pub/my-ui/nav-header/filter-dialog.vue'
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region State
|
||||||
|
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
|
||||||
|
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||||
|
entityName: 'patient',
|
||||||
|
})
|
||||||
|
|
||||||
|
const refSearchNav: RefSearchNav = {
|
||||||
|
onClick: () => {
|
||||||
|
// open filter modal
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
},
|
||||||
|
onInput: (val: string) => {
|
||||||
|
searchInput.value = val
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
searchInput.value = ''
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const refExportNav: RefExportNav = {
|
||||||
|
onExportCsv: () => {
|
||||||
|
// open filter modal
|
||||||
|
console.log(`Export CSV Clicked`)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormEntryDialogOpen = ref(false)
|
||||||
|
const isHistoryDialogOpen = ref(false)
|
||||||
|
const isRecordConfirmationOpen = ref(false)
|
||||||
|
const summaryLoading = ref(false)
|
||||||
|
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: "Surat Kontrol",
|
||||||
|
icon: 'i-lucide-newspaper',
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterPrep: HeaderPrep = {
|
||||||
|
title: "Surat Kontrol",
|
||||||
|
icon: 'i-lucide-newspaper',
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle Hooks
|
||||||
|
onMounted(() => {
|
||||||
|
getPatientSummary()
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
async function getPatientSummary() {
|
||||||
|
try {
|
||||||
|
summaryLoading.value = true
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching patient summary:', error)
|
||||||
|
} finally {
|
||||||
|
summaryLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFiltering() {
|
||||||
|
console.log('Confirmed action: Filter')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle confirmation result
|
||||||
|
async function handleConfirmDelete(record: any, action: string) {
|
||||||
|
console.log('Confirmed action:', action, 'for record:', record)
|
||||||
|
|
||||||
|
if (action === 'delete' && record?.id) {
|
||||||
|
try {
|
||||||
|
const result = await removePatient(record.id)
|
||||||
|
if (result.success) {
|
||||||
|
console.log('Patient deleted successfully')
|
||||||
|
// Refresh the list
|
||||||
|
await fetchData()
|
||||||
|
} else {
|
||||||
|
console.error('Failed to delete patient:', result)
|
||||||
|
// Handle error - show error message to user
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting patient:', error)
|
||||||
|
// Handle error - show error message to user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelConfirmation() {
|
||||||
|
// Reset record state when cancelled
|
||||||
|
recId.value = 0
|
||||||
|
recAction.value = ''
|
||||||
|
recItem.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportCsv() {
|
||||||
|
console.log('Ekspor CSV dipilih')
|
||||||
|
// tambahkan logic untuk generate CSV
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportExcel() {
|
||||||
|
console.log('Ekspor Excel dipilih')
|
||||||
|
// tambahkan logic untuk generate Excel
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Provide
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
watch([recId, recAction], () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showProcess:
|
||||||
|
navigateTo('https://google.com', { external: true, open: { target: '_blank' } });
|
||||||
|
break
|
||||||
|
|
||||||
|
case ActionEvents.showDetail:
|
||||||
|
isHistoryDialogOpen.value = true
|
||||||
|
break
|
||||||
|
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
// Trigger confirmation modal open
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header :prep="{ ...headerPrep }" />
|
||||||
|
|
||||||
|
<!-- Disable dulu, ayahab kalo diminta beneran -->
|
||||||
|
<!-- <div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
|
||||||
|
<template v-if="summaryLoading">
|
||||||
|
<SummaryCard
|
||||||
|
v-for="n in 4"
|
||||||
|
:key="n"
|
||||||
|
is-skeleton
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<SummaryCard
|
||||||
|
v-for="card in summaryData"
|
||||||
|
:key="card.title"
|
||||||
|
:stat="card"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<FilterDialog :prep="{ ...filterPrep }"
|
||||||
|
:ref-search-nav="refSearchNav"
|
||||||
|
:ref-export-nav="refExportNav"
|
||||||
|
:enable-search="false"
|
||||||
|
:enable-date-range="false"/>
|
||||||
|
|
||||||
|
<AppBpjsControlLetterList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
v-model:open="isFormEntryDialogOpen"
|
||||||
|
title="Filter"
|
||||||
|
>
|
||||||
|
<AppBpjsControlLetterFilter @submit="handleFiltering" />
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
v-model:open="isHistoryDialogOpen"
|
||||||
|
title="Log History Surat Kontrol">
|
||||||
|
<AppBpjsControlLetterCommonHistoryDialog />
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<RecordConfirmation v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
|
||||||
|
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation">
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>ID:</strong>
|
||||||
|
{{ record?.id }}
|
||||||
|
</p>
|
||||||
|
<p v-if="record?.firstName">
|
||||||
|
<strong>Nama:</strong>
|
||||||
|
{{ record.firstName }}
|
||||||
|
</p>
|
||||||
|
<p v-if="record?.code">
|
||||||
|
<strong>Kode:</strong>
|
||||||
|
{{ record.cellphone }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Badge } from '~/components/pub/ui/badge'
|
||||||
|
import { activeStatusCodes } from '~/lib/constants';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rec: any
|
||||||
|
idx?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
const code: keyof typeof activeStatusCodes = props.rec.status_code === 1 ? `active` : `inactive`
|
||||||
|
return activeStatusCodes[code]
|
||||||
|
})
|
||||||
|
|
||||||
|
const badgeVariant = computed(() => {
|
||||||
|
return props.rec.status_code === 1 ? 'default' : 'destructive'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<Badge :variant="badgeVariant" class="rounded-2xl text-[0.6rem]" >
|
||||||
|
{{ statusText }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -41,6 +41,12 @@ export interface RefSearchNav {
|
|||||||
onClear: () => void
|
onClear: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RefExportNav {
|
||||||
|
onExportPdf?: () => void
|
||||||
|
onExportCsv?: () => void
|
||||||
|
onExportExcel?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
// prepared header for relatively common usage
|
// prepared header for relatively common usage
|
||||||
export interface HeaderPrep {
|
export interface HeaderPrep {
|
||||||
title?: string
|
title?: string
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<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 { HeaderPrep, RefExportNav, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
prep: HeaderPrep
|
||||||
|
refSearchNav?: RefSearchNav
|
||||||
|
enableExport?: boolean
|
||||||
|
refExportNav?: RefExportNav
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
})
|
||||||
|
|
||||||
|
const value = ref({
|
||||||
|
start: new CalendarDate(2022, 1, 20),
|
||||||
|
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
|
||||||
|
}) as Ref<DateRange>
|
||||||
|
|
||||||
|
function onFilterClick() {
|
||||||
|
console.log('Search:', searchQuery.value)
|
||||||
|
console.log('Date Range:', dateRange.value)
|
||||||
|
props.refSearchNav?.onClick()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<div class="flex items-center gap-2 mb-4 2xl:mb-5">
|
||||||
|
|
||||||
|
<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 v-show="props.refExportNav?.onExportPdf"
|
||||||
|
@click="props.refExportNav?.onExportPdf">
|
||||||
|
Ekspor PDF
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem v-show="props.refExportNav?.onExportCsv"
|
||||||
|
@click="props.refExportNav?.onExportCsv">
|
||||||
|
Ekspor CSV
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem v-show="props.refExportNav?.onExportExcel"
|
||||||
|
@click="props.refExportNav?.onExportExcel">
|
||||||
|
Ekspor Excel
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
@@ -5,11 +5,13 @@ import type { Ref } from 'vue'
|
|||||||
import type { DateRange } from 'radix-vue'
|
import type { DateRange } from 'radix-vue'
|
||||||
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
import type { HeaderPrep, RefExportNav, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
prep: HeaderPrep
|
prep: HeaderPrep
|
||||||
refSearchNav?: RefSearchNav
|
refSearchNav?: RefSearchNav
|
||||||
|
enableExport?: boolean
|
||||||
|
refExportNav?: RefExportNav
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// function emitSearchNavClick() {
|
// function emitSearchNavClick() {
|
||||||
@@ -48,7 +50,7 @@ function onFilterClick() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<div class="flex items-center space-x-2 mb-4 2xl:mb-5">
|
<div class="flex items-center gap-2 mb-4 2xl:mb-5">
|
||||||
<div class="relative w-64">
|
<div class="relative w-64">
|
||||||
<Search class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-gray-400" />
|
<Search class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-gray-400" />
|
||||||
<Input v-model="searchQuery" type="text" placeholder="Cari Nama /No.RM" class="pl-9" />
|
<Input v-model="searchQuery" type="text" placeholder="Cari Nama /No.RM" class="pl-9" />
|
||||||
@@ -88,6 +90,30 @@ function onFilterClick() {
|
|||||||
<FilterIcon class="mr-2 size-4" />
|
<FilterIcon class="mr-2 size-4" />
|
||||||
Filter
|
Filter
|
||||||
</Button>
|
</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 v-show="props.refExportNav?.onExportPdf"
|
||||||
|
@click="props.refExportNav?.onExportPdf">
|
||||||
|
Ekspor PDF
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem v-show="props.refExportNav?.onExportCsv"
|
||||||
|
@click="props.refExportNav?.onExportCsv">
|
||||||
|
Ekspor CSV
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem v-show="props.refExportNav?.onExportExcel"
|
||||||
|
@click="props.refExportNav?.onExportExcel">
|
||||||
|
Ekspor Excel
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<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: 'Surat Kontrol',
|
||||||
|
contentFrame: 'cf-full-width',
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: () => route.meta.title as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
|
||||||
|
|
||||||
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
|
// Check if user has access to this page
|
||||||
|
const hasAccess = checkRole(roleAccess)
|
||||||
|
// if (!hasAccess) {
|
||||||
|
// navigateTo('/403')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Define permission-based computed properties
|
||||||
|
const canRead = true // hasReadAccess(roleAccess)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="canRead">
|
||||||
|
<ContentBpjsControlLetterList type="encounter" />
|
||||||
|
</div>
|
||||||
|
<Error v-else :status-code="403" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -22,12 +22,12 @@ 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({
|
// throw createError({
|
||||||
statusCode: 403,
|
// statusCode: 403,
|
||||||
statusMessage: 'Access denied',
|
// statusMessage: 'Access denied',
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canCreate = true // hasCreateAccess(roleAccess)
|
const canCreate = true // hasCreateAccess(roleAccess)
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ 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 = true // hasReadAccess(roleAccess)
|
const canRead = true // hasReadAccess(roleAccess)
|
||||||
|
|||||||
@@ -199,6 +199,11 @@
|
|||||||
"title": "Peserta",
|
"title": "Peserta",
|
||||||
"icon": "i-lucide-circuit-board",
|
"icon": "i-lucide-circuit-board",
|
||||||
"link": "/integration/bpjs/member"
|
"link": "/integration/bpjs/member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Surat Kontrol",
|
||||||
|
"icon": "i-lucide-circuit-board",
|
||||||
|
"link": "/integration/bpjs/control-letter"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user