Feat: UI KFR

This commit is contained in:
hasyim_kai
2025-11-24 10:21:20 +07:00
parent 399c3cbaee
commit f060ed33d2
25 changed files with 1759 additions and 2 deletions
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { cn } from '~/lib/utils';
const props = withDefaults(defineProps<{
assesmentDate?: string
class?: string
}>(), {
assesmentDate: new Date().toISOString(),
})
</script>
<template>
<div :class="cn('flex items-center gap-3 p-3 rounded-md text-orange-500 border border-orange-400 bg-orange-50',
props.class
)">
<Icon name="i-lucide-triangle-alert" class="h-9 w-9 align-middle transition-colors" />
<p class="font-medium">Pasien telah mencapai atau telah melampaui jadwal Asesment pada
<b>{{ new Date(props.assesmentDate).toDateString() }}</b>
<br>
Harap melakukan Re-Asement sebelum melanjutkan Protocol Therapy</p>
</div>
</template>
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { ActionEvents, type ListItemDto } from '~/components/pub/my-ui/data/types';
import Button from '~/components/pub/ui/button/Button.vue';
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 recDate = inject<Ref<any>>('rec_date')!
function confirm() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmVerify
recItem.value = props.rec
recDate.value = new Date().getTime()
}
</script>
<template>
<Button type="button" variant="outline" class="text-orange-500 border border-orange-400 bg-orange-50"
@click="confirm">
<Icon name="i-lucide-circle-check" class="h-4 w-4 align-middle transition-colors" />
Konfirmasi
</Button>
</template>
@@ -0,0 +1,21 @@
<script setup lang="ts">
import { ActionEvents, type ListItemDto } from '~/components/pub/my-ui/data/types';
import Button from '~/components/pub/ui/button/Button.vue';
const props = defineProps<{
}>()
const isModalOpen = inject<Ref<boolean>>('isHistoryDialogOpen')!
function openDialog() {
isModalOpen.value = true
}
</script>
<template>
<Button type="button" variant="outline" class="text-orange-500 border border-orange-400 bg-orange-50"
@click="openDialog">
<Icon name="i-lucide-history" class="h-4 w-4 align-middle transition-colors" />
History
</Button>
</template>
@@ -0,0 +1,66 @@
<script setup lang="ts">
const props = defineProps<{
rec: any
idx?: number
}>()
</script>
<template>
<table>
<tbody>
<tr>
<td><b>S : </b></td>
<td>{{ props.rec.result.s }}</td>
</tr>
</tbody>
</table>
<Separator class="my-3" />
<table>
<tbody>
<tr>
<td><b>O : </b></td>
<td>{{ props.rec.result.o }}</td>
</tr>
</tbody>
</table>
<Separator class="my-3" />
<table>
<tbody>
<tr>
<td><b>A : </b></td>
<td>{{ props.rec.result.a }}</td>
</tr>
</tbody>
</table>
<Separator class="my-3" />
<div>
<h1><b>P : </b></h1>
<ul class="pl-5 list-disc space-y-1">
<li>
<h1><b>Goal of Treatment</b></h1>
<p>{{ props.rec.result.p.goal }}</p>
</li>
<li>
<h1><b>Tindakan/Program Rehabilitasi Medik</b></h1>
<p>{{ props.rec.result.p.action }}</p>
</li>
<li>
<h1><b>Edukasi</b></h1>
<p>{{ props.rec.result.p.education }}</p>
</li>
<li>
<h1><b>Frekuensi Kunjungan</b></h1>
<p>{{ props.rec.result.p.frequency }} x Perminggu</p>
</li>
</ul>
</div>
<Separator class="my-3" />
<table>
<tbody>
<tr>
<td><b>Rencana Tindak Lanjut : </b></td>
<td>{{ props.rec.result.plan }} - {{ props.rec.result.planDesc }}</td>
</tr>
</tbody>
</table>
</template>
@@ -0,0 +1,100 @@
<script setup lang="ts">
import { ActionEvents, type LinkItem, type ListItemDto } from '~/components/pub/my-ui/data/types';
const props = defineProps<{
rec: ListItemDto
}>()
const { getActiveRole } = useUserStore()
const recId = inject<Ref<number>>('rec_id')!
const recAction = inject<Ref<string>>('rec_action')!
const recItem = inject<Ref<any>>('rec_item')!
const timestamp = inject<Ref<any>>('timestamp')!
const activeKey = ref<string | null>(null)
const linkItems = computed(() => {
const role = getActiveRole()
const isAdmin = role == "system"
const isDoctorRole = role == "emp|doc"
const isPhysioRole = role == "emp|doc"
const isUnverified = true // recItem.id === 0
const isUnvalidated = true // recItem.id
const items: LinkItem[] = [
{ label: 'Print', onClick: print, icon: 'i-lucide-printer', }
]
if (isDoctorRole || isAdmin) {
items.push({ label: 'Edit', onClick: edit, icon: 'i-lucide-pencil', })
if (isUnverified) {
items.push({ label: 'Verify', onClick: verify, icon: 'i-lucide-check', })
}
if (!isUnverified && isUnvalidated) { // verified & unvalidated
items.push({ label: 'Validate', onClick: validate, icon: 'i-lucide-check-check', })
}
items.push({ label: 'Delete', onClick: del, icon: 'i-lucide-trash', })
}
return items
})
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
timestamp.value = new Date().getTime()
}
function verify() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showVerify
recItem.value = props.rec
timestamp.value = new Date().getTime()
}
function validate() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showValidate
recItem.value = props.rec
timestamp.value = new Date().getTime()
}
function print() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showPrint
recItem.value = props.rec
timestamp.value = new Date().getTime()
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
timestamp.value = new Date().getTime()
}
</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,93 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { Label as RadioLabel } from '~/components/pub/ui/label'
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
errors?: FormErrors
class?: string
radioGroupClass?: string
radioItemClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'isNewBorn',
label = 'Status Pasien',
errors,
class: containerClass,
radioGroupClass,
radioItemClass,
labelClass,
} = props
const newbornOptions = [
{ value: 'EVALUASI', label: 'Evaluasi' },
{ value: 'RUJUK', label: 'Rujuk' },
{ value: 'SELESAI', label: 'Selesai' },
]
</script>
<template>
<DE.Cell :class="cn('radio-group-field', containerClass)" :col-span="2">
<DE.Label
:label-for="fieldName"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<RadioGroup
v-bind="componentField"
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
>
<div
v-for="(option, index) in newbornOptions"
:key="option.value"
:class="cn('flex min-w-fit items-center space-x-2 pt-1', radioItemClass)"
>
<RadioGroupItem
:id="`${fieldName}-${index}`"
:value="option.value"
:class="
cn(
'relative h-4 w-4 rounded-full border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
containerClass,
)
"
/>
<RadioLabel
:for="`${fieldName}-${index}`"
:class="
cn(
'cursor-pointer select-none text-xs font-normal leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
labelClass,
)
"
>
{{ option.label }}
</RadioLabel>
</div>
</RadioGroup>
</FormControl>
<FormMessage class="ml-0 mt-1" />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { Badge } from '~/components/pub/ui/badge'
const props = defineProps<{
rec: any
idx?: number
}>()
const verifyStatusCodes: Record<string, string> = {
verified: 'Terverifikasi',
unverified: 'Belum Verifikasi',
}
const validateStatusCodes: Record<string, string> = {
validated: 'Tervalidasi',
unvalidated: 'Belum Validasi',
}
const verifyStatusText = computed(() => {
const code: keyof typeof verifyStatusCodes = props.rec.status.verified === 1 ? `verified` : `unverified`
return verifyStatusCodes[code]
})
const validateStatusText = computed(() => {
const code: keyof typeof validateStatusCodes = props.rec.status.validated === 1 ? `validated` : `unvalidated`
return validateStatusCodes[code]
})
const verifyBadgeVariant = computed(() => {
return props.rec.status.verified === 1 ? 'default' : 'outline'
})
const validateBadgeVariant = computed(() => {
return props.rec.status.validated === 1 ? 'default' : 'outline'
})
</script>
<template>
<div class="flex flex-col gap-2 items-center justify-center">
<Badge :variant="verifyBadgeVariant" class="w-fit rounded-2xl text-[0.6rem]" >
{{ verifyStatusText }}
</Badge>
<Badge :variant="validateBadgeVariant" class="w-fit rounded-2xl text-[0.6rem]" >
{{ validateStatusText }}
</Badge>
</div>
</template>
+128
View File
@@ -0,0 +1,128 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import { Form } from '~/components/pub/ui/form'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import * as DE from '~/components/pub/my-ui/doc-entry'
import TextAreaInput from '~/components/pub/my-ui/form/text-area-input.vue'
import { cn } from '~/lib/utils'
import RadioFollowup from './_common/radio-followup.vue'
const props = defineProps<{
schema: any
initialValues?: any
errors?: FormErrors
}>()
const formSchema = toTypedSchema(props.schema)
const formRef = ref()
// const isMedicalDiagnosisPickerDialogOpen = ref<boolean>(false)
// const isFunctionalDiagnosisPickerDialogOpen = ref<boolean>(false)
// const isProcedurePickerDialogOpen = ref<boolean>(false)
// function toggleMedicalDiagnosisPickerDialog() {
// isMedicalDiagnosisPickerDialogOpen.value = !isMedicalDiagnosisPickerDialogOpen.value
// }
// function toggleFunctionalDiagnosisPickerDialog() {
// isFunctionalDiagnosisPickerDialogOpen.value = !isFunctionalDiagnosisPickerDialogOpen.value
// }
// provide(`isDiagnosisPickerDialogOpen`, isDiagnosisPickerDialogOpen)
// provide(`isProcedurePickerDialogOpen`, isProcedurePickerDialogOpen)
defineExpose({
validate: () => formRef.value?.validate(),
resetForm: () => formRef.value?.resetForm(),
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
values: computed(() => formRef.value?.values),
})
</script>
<template>
<Form ref="formRef"
v-slot="{ values }"
as=""
keep-values
:validation-schema="formSchema"
:validate-on-mount="false"
validation-mode="onSubmit"
:initial-values="initialValues">
<!-- FORM 1 -->
<DE.Block :col-count="2" :cell-flex="false">
<DE.Cell :col-span="2">
<TextAreaInput
field-name="subjective"
label="Subjective"
placeholder="Subjective"
class="w-1/2"
:errors="errors" />
</DE.Cell>
<DE.Cell :col-span="2" >
<TextAreaInput
field-name="objective"
label="Objective"
placeholder="Masukkan Objective"
class="w-1/2"
:errors="errors" />
</DE.Cell>
<DE.Cell :col-span="2">
<TextAreaInput
field-name="assesment"
label="Assesment"
placeholder="Masukkan Assesment"
class="w-1/2"
:errors="errors" />
</DE.Cell>
<DE.Cell class="mt-2 px-4 bg-gray-50 border rounded-lg" :col-span="2">
<DE.Block :col-count="2" :cell-flex="false">
<TextAreaInput
field-name="planningGoal"
label="Goal of Treatment"
placeholder="Masukkan Goal of Treatment"
:errors="errors" />
<TextAreaInput
field-name="planningAction"
label="Tindakan/Program Rehabilitasi Medik"
placeholder="Masukkan Tindakan/Program Rehabilitasi Medik"
:errors="errors" />
<TextAreaInput
field-name="planningEducation"
label="Edukasi"
placeholder="Masukkan Edukasi"
:errors="errors" />
<InputBase
field-name="planningFrequency"
label="Frekuensi Kunjungan"
right-label="x Minggu"
placeholder="Masukkan Frekuensi Kunjungan"
:errors="errors"
numeric-only
is-disabled
/>
</DE.Block>
</DE.Cell>
<DE.Cell :col-span="2">
<RadioFollowup
field-name="followUpPlan"
label="Rencana Tindak Lanjut"
:errors="errors"
is-required
/>
<TextAreaInput
label=""
field-name="followUpPlanDesc"
placeholder="Masukkan Keterangan rencana tindak lanjut"
class="w-1/2 mt-3"
:errors="errors" />
</DE.Cell>
</DE.Block>
</Form>
</template>
@@ -0,0 +1,60 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import type { Patient } from '~/models/patient'
import { defineAsyncComponent } from 'vue'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dp.vue'))
const resultData = defineAsyncComponent(() => import('./_common/card-result.vue'))
const statusBadge = defineAsyncComponent(() => import('./_common/verify-badge.vue'))
export const config: Config = {
cols: [{}, { width: 800 }, {}, { width: 120 }, { width: 3 },],
headers: [
[
{ label: 'Tanggal' },
{ label: 'Hasil Asesmen Pasien Dan Pemberian Pelayanan' },
{ label: 'Jenis Form' },
{ label: 'Status' },
{ label: 'Action' },
],
],
keys: ['date', 'result', 'type', 'status', 'action'],
parses: {
date: (rec: unknown): unknown => {
const date = (rec as any).date
if (typeof date == 'object' && date) {
return (date as Date).toLocaleDateString('id-ID')
} else if (typeof date == 'string') {
return (date as string).substring(0, 10)
}
return date
},
},
components: {
result(rec, idx) {
return {
idx,
rec: rec as object,
component: resultData,
}
},
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
status(rec, idx) {
return {
idx,
rec: rec as object,
component: statusBadge,
}
},
},
}
+61
View File
@@ -0,0 +1,61 @@
<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 './history-list.cfg'
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
import type { DateRange } from 'radix-vue'
import { cn } from '~/lib/utils'
interface Props {
data: any[]
paginationMeta: PaginationMeta
dateValue: DateRange
}
const props = defineProps<Props>()
const df = new DateFormatter('en-US', { dateStyle: 'medium',})
const emit = defineEmits<{
pageChange: [page: number]
'update:dateValue': [value: DateRange]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" :class="cn('mb-1 w-[280px] justify-start text-left font-normal',
!props.dateValue && 'text-muted-foreground')">
<CalendarIcon class="mr-2 h-4 w-4" />
<template v-if="props.dateValue.start">
<template v-if="props.dateValue.end">
{{ df.format(props.dateValue.start.toDate(getLocalTimeZone())) }} -
{{ df.format(props.dateValue.end.toDate(getLocalTimeZone())) }}
</template>
<template v-else>
{{ df.format(props.dateValue.start.toDate(getLocalTimeZone())) }}
</template>
</template>
<template v-else> Pick a date </template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<RangeCalendar v-model="props.dateValue" initial-focus :number-of-months="2"
@update:model-value="(date) => emit('update:dateValue', date)" />
</PopoverContent>
</Popover>
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
+59
View File
@@ -0,0 +1,59 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
const action = defineAsyncComponent(() => import('./_common/dropdown-action.vue'))
const statusBadge = defineAsyncComponent(() => import('./_common/verify-badge.vue'))
const resultData = defineAsyncComponent(() => import('./_common/card-result.vue'))
export const config: Config = {
cols: [{}, { width: 800 }, {}, { width: 120 }, { width: 3 },],
headers: [
[
{ label: 'Tanggal' },
{ label: 'Hasil Asesmen Pasien Dan Pemberian Pelayanan' },
{ label: 'Jenis Form' },
{ label: 'Status' },
{ label: 'Action' },
],
],
keys: ['date', 'result', 'type', 'status', 'action'],
parses: {
date: (rec: unknown): unknown => {
const date = (rec as any).date
if (typeof date == 'object' && date) {
return (date as Date).toLocaleDateString('id-ID')
} else if (typeof date == 'string') {
return (date as string).substring(0, 10)
}
return date
},
},
components: {
result(rec, idx) {
return {
idx,
rec: rec as object,
component: resultData,
}
},
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
status(rec, idx) {
return {
idx,
rec: rec as object,
component: statusBadge,
}
},
},
}
+14
View File
@@ -0,0 +1,14 @@
<script setup lang="ts">
import { config } from './list.cfg'
interface Props {
data: any[]
}
defineProps<Props>()
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable v-bind="config" :rows="data" />
</div>
</template>
+108
View File
@@ -0,0 +1,108 @@
<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 type { InstallationFormData } from '~/schemas/installation.schema'
import TextCaptcha from '~/components/pub/my-ui/form/text-captcha.vue'
const props = defineProps<{
schema: any
errors?: FormErrors
}>()
const emit = defineEmits<{
submit: [values: InstallationFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const formSchema = toTypedSchema(props.schema)
const formRef = ref()
const captchaRef = ref<InstanceType<typeof TextCaptcha> | null>(null)
const captchaValid = ref(false)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: InstallationFormData = {
name: values.name || '',
code: values.code || '',
}
emit('submit', formData, resetForm)
}
function onCaptchaUpdate(valid: boolean) {
captchaValid.value = valid
}
// Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) {
emit('cancel', resetForm)
}
defineExpose({
validate: () => formRef.value?.validate(),
resetForm: () => formRef.value?.resetForm(),
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
values: computed(() => formRef.value?.values),
})
</script>
<template>
<Form
ref="formRef"
v-slot="{ values }"
as=""
keep-values
:validation-schema="formSchema"
validation-mode="onSubmit"
>
<div class="border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<InputBase
field-name="name"
label="Nama"
placeholder="Masukkan Nama"
:errors="errors"/>
<InputBase
field-name="email"
label="Email"
placeholder="Masukkan Email"
:errors="errors"/>
<div class="mt-2">
<Label class="" for="password">Password</Label>
<Field class="" id="password" :errors="errors">
<FormField v-slot="{ componentField }" name="password">
<FormItem>
<FormControl>
<Input
id="password"
v-bind="componentField"
type="password"
class="w-full"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</div>
<TextCaptcha
ref="captchaRef"
:length="5"
:useSpacing="true"
:noiseChars="true"
@update:valid="onCaptchaUpdate"
/>
</div>
</div>
</Form>
</template>
@@ -14,6 +14,7 @@ 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 KfrList from '~/components/content/kfr/list.vue'
import TherapyProtocolList from '~/components/content/therapy-protocol/list.vue'
import Prescription from '~/components/content/prescription/main.vue'
import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
@@ -54,6 +55,12 @@ const tabs: TabItem[] = [
component: EarlyMedicalRehabList,
props: { encounter: data, type: 'early-rehab', label: 'Pengkajian Awal Medis Rehabilitasi Medis' },
},
{
value: 'kfr',
label: 'Formulir Rawat Jalan KFR',
component: KfrList,
props: { encounter: data, type: 'kfr', label: 'Formulir Rawat Jalan KFR' },
},
{
value: 'function-assessment',
label: 'Asesmen Fungsi',
+138
View File
@@ -0,0 +1,138 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import type { ExposedForm } from '~/types/form'
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
import { toast } from '~/components/pub/ui/toast'
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
import { KfrSchema, } from '~/schemas/kfr.schema'
import { handleActionSave } from '~/handlers/kfr.handler'
import { getDetail } from '~/services/kfr.service';
// #region Props & Emits
const props = withDefaults(defineProps<{
callbackUrl?: string
mode?: 'add' | 'edit'
}>(), {
mode: "add",
})
// form related state
const route = useRoute()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const kfrId = typeof route.params.kfr_id == 'string' ? parseInt(route.params.kfr_id) : 0
const inputForm = ref<ExposedForm<any> | null>(null)
// #endregion
// #region State & Computed
const router = useRouter()
const isConfirmationOpen = ref(false)
// #endregion
// #region Lifecycle Hooks
onMounted(() => {
if(props.mode === `edit`) init()
})
// #endregion
// #region Functions
async function init(){
const result = await getDetail(kfrId)
if (result.success) {
const currentValue = result.body?.data || {}
inputForm.value?.setValues(currentValue)
}
}
function goBack() {
router.go(-1)
}
async function handleConfirmAdd() {
const inputData: any = await composeFormData()
const response = await handleActionSave(
inputData,
() => { },
() => { },
toast,
)
const data = (response?.body?.data ?? null)
if (!data) return
goBack()
}
async function composeFormData(): Promise<any> {
const [input,] = await Promise.all([
inputForm.value?.validate(),
])
const results = [input]
const allValid = results.every((r) => r?.valid)
// exit, if form errors happend during validation
if (!allValid) return Promise.reject('Form validation failed')
const formData = input?.values
formData.encounter_id = encounterId
return new Promise((resolve) => resolve(formData))
}
// #endregion region
// #region Utilities & event handlers
async function handleActionClick(eventType: string) {
if (eventType === 'submit') {
isConfirmationOpen.value = true
}
if (eventType === 'back') {
if (props.callbackUrl) return navigateTo(props.callbackUrl)
goBack()
}
}
function handleCancelAdd() {
isConfirmationOpen.value = false
}
// #endregion
// #region Watchers
// #endregion
const initial = {
// subjective: '',
// objective: '',
// assesment: '',
// planningGoal: '',
// planningAction: '',
// planningEducation: '',
planningFrequency: 2,
followUpPlan: 'EVALUASI',
// followUpPlanDesc: '',
}
</script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">
{{ props.mode === "add" ? `Tambah` : `Update` }} Formulir Rawat Jalan KFR
</div>
<AppKfrEntry
ref="inputForm"
:schema="KfrSchema"
:initial-values="initial"
/>
<div class="my-2 flex justify-end py-2">
<Action :enable-draft="false" @click="handleActionClick"/>
</div>
<Confirmation
v-model:open="isConfirmationOpen"
title="Simpan Data"
message="Apakah Anda yakin ingin menyimpan data ini?"
confirm-text="Simpan"
@confirm="handleConfirmAdd"
@cancel="handleCancelAdd"
/>
</template>
+372
View File
@@ -0,0 +1,372 @@
<script setup lang="ts">
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
// #region Imports
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { getList, remove } from '~/services/kfr.service'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
import type { Encounter } from '~/models/encounter'
import { cn } from '~/lib/utils'
import type { ExposedForm } from '~/types/form'
import { VerificationSchema } from '~/schemas/verification.schema'
import { handleActionSave } from '~/handlers/kfr.handler'
import { toast } from '~/components/pub/ui/toast'
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { CalendarDate } from '@internationalized/date'
import type { DateRange } from 'radix-vue'
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
// #endregion
// Props
interface Props {
encounter: Encounter
}
const props = defineProps<Props>()
const route = useRoute()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const kfrId = typeof route.params.kfr_id == 'string' ? parseInt(route.params.kfr_id) : 0
// #endregion
// #region State
const { getActiveRole } = useUserStore()
const isVerifyDialogOpen = ref(false)
const isValidateDialogOpen = ref(false)
const isHistoryDialogOpen = ref(false)
const isPatientInTherapy = ref(false)
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
fetchFn: (params) => getList({ ...params }),
entityName: 'kfr',
})
const historyData = usePaginatedList({
fetchFn: (params) => getList({ ...params }),
entityName: 'kfr-history',
})
const dummy = [
{
id: 11,
date: new Date(),
result: {
s: `Example`,
o: `Example`,
a: `Example`,
p: {
goal: `Example`,
action: `Example`,
education: `Example`,
frequency: `Example`,
},
plan: `Example`,
planDesc: `Description`,
},
type: `Asesmen`,
status: {
verified: 1,
validated: 0,
},
}
]
const isRecordConfirmationOpen = ref(false)
const isDocPreviewDialogOpen = ref(false)
const summaryLoading = ref(false)
const inputForm = ref<ExposedForm<any> | null>(null)
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const timestamp = ref<number>(0)
const isAssessment = ref<boolean>(false)
const isTherapyProtocol = ref<boolean>(false)
const isReassessment = ref<boolean>(true)
const isInOrBeyondAssessmentPeriod = ref<boolean>(true)
const isDoctor = computed(() => getActiveRole() === 'emp|doc')
const isAdmin = computed(() => getActiveRole() === 'system')
const isCaptchaValid = ref(false)
provide('isCaptchaValid', isCaptchaValid)
const addBtnTxt = computed(() => {
if (isAssessment.value) {
return `Tambah Asesmen`
} else if (isTherapyProtocol.value) {
return `Tambah Protokol Terapi`
} else if (isReassessment.value) {
return `Tambah Re-Asesmen`
}
return `Tambah Asesmen`
})
const headerPrep: HeaderPrep = {
title: "Formulir Rawat Jalan KFR",
icon: 'i-lucide-newspaper',
}
if(isDoctor.value || isAdmin.value) {
headerPrep.addNav = {
label: addBtnTxt.value,
onClick: () => navigateTo({
name: 'rehab-encounter-id-kfr-add',
}),
}
}
if(!isAssessment.value) {
headerPrep.components = [
{
component: defineAsyncComponent(() => import('~/components/app/kfr/_common/btn-history.vue')),
props: { }
},
];
}
const defaultDate = {
start: new CalendarDate(2025, 1, 20),
end: new CalendarDate(2025, 1, 20).add({ days: 20 }),
}
const historyDateValue = ref(defaultDate) as Ref<DateRange>
// #endregion
// #region Lifecycle Hooks
onMounted(() => {
getPatientSummary()
const isInTherapy = false // TODO: determine if patient is in therapy
handleIsPatientInTherapy(isInTherapy)
})
// #endregion
// #region Functions
function handleIsInAssesmentPeriood(value: boolean) {
if (value) {
isInOrBeyondAssessmentPeriod.value = true
} else {
isInOrBeyondAssessmentPeriod.value = false
}
}
function handleIsPatientInTherapy(value: boolean) {
if (value) {
isPatientInTherapy.value = true
} else {
isPatientInTherapy.value = false
}
}
async function handleOpenHistory() {
isHistoryDialogOpen.value = true
}
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 toggleHistoryDialog() {
isHistoryDialogOpen.value = !isHistoryDialogOpen.value
}
function handleVerify() {
isVerifyDialogOpen.value = true
}
async function handleConfirmVerify() {
const inputData: any = await composeFormData()
const response = await handleActionSave(
inputData,
() => { },
() => { },
toast,
)
const data = (response?.body?.data ?? null)
if (!data) return
isVerifyDialogOpen.value = false
}
async function composeFormData(): Promise<any> {
const [input,] = await Promise.all([
inputForm.value?.validate(),
])
const results = [input]
const allValid = results.every((r) => r?.valid)
// exit, if form errors happend during validation
if (!allValid) return Promise.reject('Form validation failed')
const formData = input?.values
// formData.encounter_id = encounterId
return new Promise((resolve) => resolve(formData))
}
async function handleConfirmValidate() {
try {
// const result = await remove(record.id)
// if (result.success) {
// toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
// await fetchData()
// } else {
// toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
// }
} catch (error) {
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
}
}
function handleCancelValidate() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
async function handleConfirmDelete(record: any, action: string) {
if (action === 'delete' && record?.id) {
try {
const result = await remove(record.id)
if (result.success) {
toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
await fetchData()
} else {
toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
}
} catch (error) {
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
}
}
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion region
// #region Utilities & event handlers
async function handleActionClick(eventType: string) {
if (eventType === 'submit') {
handleConfirmVerify()
}
if (eventType === 'back') {
isVerifyDialogOpen.value = false
}
}
// #endregion
// #region Provide
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('timestamp', timestamp)
provide('table_data_loader', isLoading)
provide('isHistoryDialogOpen', isHistoryDialogOpen)
// #endregion
// #region Watchers
watch([recId, recAction, timestamp], () => {
switch (recAction.value) {
case ActionEvents.showEdit:
// if(pagePermission.canUpdate) {
navigateTo({
name: 'rehab-encounter-id-kfr-kfr_id-edit',
params: {
kfr_id: kfrId
}
})
// } else {
// unauthorizedToast()
// }
break
case ActionEvents.showVerify:
// if(pagePermission.canUpdate) {
handleVerify()
// } else {
// unauthorizedToast()
// }
break
case ActionEvents.showValidate:
// if(pagePermission.canUpdate) {
isValidateDialogOpen.value = true
// } else {
// unauthorizedToast()
// }
break
case ActionEvents.showPrint:
isDocPreviewDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
// #endregion
</script>
<template>
<Header :prep="{ ...headerPrep }" />
<AppKfrCommonBannerPatientInTherapy v-if="isInOrBeyondAssessmentPeriod" class="mb-5" />
<AppKfrList :data="dummy" />
<Dialog v-model:open="isVerifyDialogOpen" title="Verifikasi">
<AppKfrVerifyDialog ref="inputForm" :schema="VerificationSchema" />
<div class="flex justify-end">
<Action v-show="isCaptchaValid" :enable-draft="false" @click="handleActionClick" />
</div>
</Dialog>
<Confirmation
v-model:open="isValidateDialogOpen"
title="Validasi Data"
message="Apakah Anda yakin ingin Validasi data ini?"
confirm-text="Simpan"
@confirm="handleConfirmValidate"
@cancel="handleCancelValidate"
/>
<Dialog v-model:open="isHistoryDialogOpen" title="History" size="full">
<AppKfrHistoryList
:data="dummy"
v-model:date-value="historyDateValue"
:pagination-meta="paginationMeta"
@page-change="handlePageChange" />
</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>
</div>
</template>
</RecordConfirmation>
<Dialog v-model:open="isDocPreviewDialogOpen" title="Preview Dokumen" size="2xl">
<!-- <DocPreviewDialog :link="recItem.url" /> -->
<DocPreviewDialog :link="`https://www.antennahouse.com/hubfs/xsl-fo-sample/pdf/basic-link-1.pdf`" />
</Dialog>
</template>
@@ -0,0 +1,103 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from './types'
import { ActionEvents } from './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: 'Detail',
onClick: () => {
detail()
},
icon: 'i-lucide-eye',
},
{
label: 'Verifikasi',
onClick: () => {
verify()
},
icon: 'i-lucide-check',
},
{
label: 'Validasi',
onClick: () => {
validate()
},
icon: 'i-lucide-check-check',
},
{
label: 'Print',
onClick: () => {
print()
},
icon: 'i-lucide-printer',
},
]
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function verify() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showVerify
recItem.value = props.rec
}
function validate() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showValidate
recItem.value = props.rec
}
function print() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showPrint
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>
+2
View File
@@ -78,7 +78,9 @@ export const ActionEvents = {
showEdit: 'showEdit',
showDetail: 'showDetail',
showProcess: 'showProcess',
showVerify: 'showVerify',
showConfirmVerify: 'showConfirmVerify',
showValidate: 'showValidate',
showPrint: 'showPrint',
}
@@ -47,7 +47,7 @@ function handleInput(event: Event) {
<template>
<DE.Cell :col-span="colSpan || 1">
<DE.Label
class="mb-2 font-medium"
class="font-medium"
v-if="label !== ''"
:label-for="fieldName"
:is-required="isRequired && !isDisabled"
@@ -69,7 +69,7 @@ function handleInput(event: Event) {
v-bind="componentField"
:placeholder="placeholder"
:maxlength="maxLength"
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0', props.class)"
autocomplete="off"
aria-autocomplete="none"
autocorrect="off"
@@ -0,0 +1,175 @@
<script setup lang="ts">
import { ref, computed, watch, defineEmits, defineProps, onMounted, nextTick, defineExpose } from 'vue'
import Input from '~/components/pub/ui/input/Input.vue';
import Button from '~/components/pub/ui/button/Button.vue';
import waveyFingerprint from '~/assets/svg/wavey-fingerprint.svg'
/**
* TextCaptcha props:
* - length: number of characters in the core captcha
* - caseSensitive: whether validation is case sensitive
* - useSpacing: show spaced-out characters (visual obfuscation only)
* - noiseChars: include random noise characters visually (not required to type)
*/
const props = defineProps({
length: { type: Number, default: 6 },
caseSensitive: { type: Boolean, default: false },
useSpacing: { type: Boolean, default: true },
noiseChars: { type: Boolean, default: false }, // adds random noise characters to display
refreshCooldownMs: { type: Number, default: 500 }, // guard repeated refresh
})
const emit = defineEmits<{
(e: 'update:valid', valid: boolean): void
(e: 'validated', valid: boolean): void
(e: 'change', value: string): void
}>()
// Internal state
const raw = ref('') // the canonical captcha value (what user must match, ignoring visual noise)
const display = ref('') // randomized visual representation (may include spacing/noise)
const input = ref('') // user typed value
const lastRefresh = ref(0)
const valid = inject('isCaptchaValid') as Ref<boolean>
const errorMessage = ref('')
/** Characters excluding ambiguous ones: 0/O, 1/l/I etc. */
const CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
function randomChar() {
return CHARS.charAt(Math.floor(Math.random() * CHARS.length))
}
/** Generate the canonical captcha string */
function genRaw(len = props.length) {
let s = ''
for (let i = 0; i < len; i++) s += randomChar()
return s
}
/** Create a visually obfuscated display string (spacing, noise, random case) */
function genDisplay(base: string) {
const arr: string[] = []
for (const ch of base) {
// toggle case randomly (only for letters)
const c = /[A-Za-z]/.test(ch) && Math.random() > 0.5 ? (Math.random() > 0.5 ? ch.toLowerCase() : ch.toUpperCase()) : ch
arr.push(c)
if (props.useSpacing && Math.random() > 0.3) arr.push(' ') // random space
}
return arr.join('')
}
/** Refresh captcha */
function refresh() {
const now = Date.now()
if (now - lastRefresh.value < props.refreshCooldownMs) return
lastRefresh.value = now
raw.value = genRaw(props.length)
display.value = genDisplay(raw.value)
input.value = ''
valid.value = false
errorMessage.value = ''
// emit change so parent knows new value (but we don't send the raw canonical in production)
emit('change', display.value)
}
/** Normalize input and canonical for comparison */
function normalizeForCompare(s: string) {
const normalized = s.replace(/\s+/g, '') // strip spaces
return props.caseSensitive ? normalized : normalized.toLowerCase()
}
/** Validate the current input */
function validate() {
const left = normalizeForCompare(input.value)
const right = normalizeForCompare(raw.value)
if (!input.value) {
valid.value = false
errorMessage.value = 'Please enter the captcha text.'
} else if (left === right) {
valid.value = true
errorMessage.value = ''
} else {
valid.value = false
errorMessage.value = 'Captcha does not match.'
}
emit('update:valid', valid.value)
emit('validated', valid.value)
return valid.value
}
// expose a refresh method to parent via ref
defineExpose({ refresh, validate, isValid: computed(() => valid.value) })
// generate on mount
onMounted(() => refresh())
// // re-validate whenever input changes (lightweight)
// watch(input, () => {
// // we don't auto-pass until the user explicitly validate (but we can optionally live-validate)
// // Here we perform live feedback but still emit validated only when called
// const left = normalizeForCompare(input.value)
// const right = normalizeForCompare(raw.value)
// valid.value = !!input.value && left === right
// // emit a live update so the parent can disable submit accordingly
// emit('update:valid', valid.value)
// })
</script>
<template>
<div class="space-y-2 w-full max-w-sm">
<div class="flex items-center justify-between gap-3">
<!-- Captcha visual box -->
<div
role="img"
aria-label="Text captcha, type the characters shown"
tabindex="0"
class="select-none p-3 rounded-md border border-gray-200 text-white text-xl font-mono tracking-wider text-center w-full"
>
<span class="inline-block" v-html="display"></span>
</div>
<!-- Refresh -->
<div class="flex-shrink-0">
<Button variant="ghost" type="button" @click="refresh" title="Refresh captcha">
<Icon name="i-lucide-refresh-cw" />
</Button>
</div>
</div>
<!-- Input -->
<div class="flex gap-3 items-start">
<div class="flex-grow">
<Input
v-model="input"
:aria-invalid="valid ? 'false' : 'true'"
inputmode="text"
placeholder="Type the captcha text"
@keyup.enter="validate"
/>
<p v-if="errorMessage" class="text-xs text-red-500 mt-1">{{ errorMessage }}</p>
<p v-else-if="valid" class="text-xs text-green-500 mt-1">Correct</p>
<p v-else class="text-xs text-gray-500 mt-1">Not case-sensitive</p>
</div>
<Button variant="outline" type="button" @click="validate" title="Validate"
class="border-orange-400">
<Icon name="i-lucide-check" class="text-orange-400" />
</Button>
</div>
</div>
</template>
<style scoped>
/* small nicety: make noise/spaced display look irregular */
div[role="img"] {
background: url('~/assets/svg/wavey-fingerprint.svg') repeat center;
}
div[role="img"] span {
letter-spacing: 0.12em;
font-weight: 600;
user-select: none;
}
</style>
+24
View File
@@ -0,0 +1,24 @@
// Handlers
import { genCrudHandler } from '~/handlers/_handler'
// Services
import { create, update, remove } from '~/services/kfr.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
@@ -0,0 +1,44 @@
<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: 'Protokol Terapi',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const { checkRole, hasCreateAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// throw createError({
// statusCode: 403,
// statusMessage: 'Access denied',
// })
// }
// Define permission-based computed properties
const canCreate = true // hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentKfrEntry mode="edit" />
</div>
<Error
v-else
:status-code="403"
/>
</template>
@@ -0,0 +1,44 @@
<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: 'Protokol Terapi',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const { checkRole, hasCreateAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// throw createError({
// statusCode: 403,
// statusMessage: 'Access denied',
// })
// }
// Define permission-based computed properties
const canCreate = true // hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentKfrEntry />
</div>
<Error
v-else
:status-code="403"
/>
</template>
+17
View File
@@ -0,0 +1,17 @@
import { z } from 'zod'
type KfrFormData = z.infer<typeof KfrSchema>
const KfrSchema = z.object({
subjective: z.string({required_error: 'Mohon lengkapi Form',}),
objective: z.string({required_error: 'Mohon lengkapi Form',}),
assesment: z.string({required_error: 'Mohon lengkapi Form',}),
planningGoal: z.string({required_error: 'Mohon lengkapi Form',}),
planningAction: z.string({required_error: 'Mohon lengkapi Form',}),
planningEducation: z.string({required_error: 'Mohon lengkapi Form',}),
planningFrequency: z.number({required_error: 'Mohon lengkapi Form',}),
followUpPlan: z.enum(['EVALUASI', 'RUJUK', "SELESAI"], {required_error: 'Mohon lengkapi status pasien', }),
followUpPlanDesc: z.string({required_error: 'Mohon lengkapi Form',}),
})
export { KfrSchema, }
export type { KfrFormData, }
+27
View File
@@ -0,0 +1,27 @@
// Base
import * as base from './_crud-base'
// Constants
const path = '/api/v1/kfr'
const name = 'kfr'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string, params?: any) {
return base.getDetail(path, id, name, params)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}