wip: init form entry education assessment

import clinical const from sheets

wip: form entry add education assessment

done: checkbox

wip: add select Asesmen Kemampuan dan Kemauan Belajar
This commit is contained in:
Khafid Prayoga
2025-10-21 11:21:06 +07:00
parent 27ab7c2e83
commit 736e951f33
12 changed files with 600 additions and 1 deletions
@@ -0,0 +1,137 @@
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { Form } from '~/components/pub/ui/form'
import * as DE from '~/components/pub/my-ui/doc-entry'
import Separator from '~/components/pub/ui/separator/Separator.vue'
import { BaseSelect, BaseTextarea, CheckboxGeneral, CheckboxSpecial } from './field'
// constant
import {
abilityCode,
willCode,
medObstacleCode,
learnMethodCode,
langClassCode,
translatorSrcCode,
} from '~/lib/clinical.constants'
const props = defineProps<{
schema: any
initialValues?: any
}>()
const formSchema = toTypedSchema(props.schema)
const formRef = ref()
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 ? initialValues : {}"
>
<p class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base">Kebutuhan Edukasi</p>
<DE.Block
:col-count="4"
:cell-flex="false"
>
<CheckboxGeneral
field-name="generalEducationNeeds"
label="Informasi Umum"
:col-span="2"
/>
<CheckboxSpecial
field-name="specificEducationNeeds"
label="Edukasi Khusus"
:col-span="2"
/>
</DE.Block>
<div class="h-6">
<Separator />
</div>
<p class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base">Asesmen Kemampuan dan Kemauan Belajar</p>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<BaseSelect
field-name="learningAbility"
label="Kemampuan Belajar"
:items="abilityCode"
/>
<BaseSelect
field-name="learningWillingness"
label="Kemauan Belajar"
:items="willCode"
/>
<BaseSelect
field-name="barrier"
label="Hambatan"
:items="medObstacleCode"
/>
<BaseSelect
field-name="learningMethod"
label="Metode Pembelajaran"
:items="learnMethodCode"
/>
<BaseSelect
field-name="language"
label="Bahasa"
:items="langClassCode"
/>
<BaseSelect
field-name="languageBarrier"
label="Hambatan Bahasa"
:items="translatorSrcCode"
/>
<BaseSelect
field-name="beliefValue"
label="Keyakinan pada Nilai-Nilai yang Dianut"
:items="{
ya: 'IYA',
tidak: 'TIDAK',
}"
/>
</DE.Block>
<div class="h-6">
<Separator />
</div>
<p class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base">Rencana Edukasi</p>
<DE.Block>
<BaseTextarea
field-name="recommendation"
label="Plan A"
/>
<BaseTextarea
field-name="recommendation"
label="Plan B"
/>
<BaseTextarea
field-name="recommendation"
label="Plan Plan Pak Supir"
/>
</DE.Block>
</Form>
</template>
@@ -0,0 +1,64 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
import { Checkbox } from '~/components/pub/ui/checkbox'
import type { CheckItem } from '~/lib/utils'
const props = defineProps<{
label: string
fieldName: string
items: CheckItem[]
colSpan?: number
}>()
const { label, fieldName, items } = props
</script>
<template>
<DE.Cell :col-span="colSpan || 1">
<DE.Label :label-for="fieldName">
{{ label }}
</DE.Label>
<DE.Field :id="fieldName">
<FormField
:name="fieldName"
v-slot="{ value, handleChange }"
>
<FormItem>
<div
v-for="item in items"
:key="item.id"
class="ml-1 flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
:id="`cb-${fieldName}-${item.id}`"
:checked="value?.includes(item.id)"
@update:checked="
(checked) => {
const newValue = [...(value || [])]
if (checked) {
if (!newValue.includes(item.id)) newValue.push(item.id)
} else {
const idx = newValue.indexOf(item.id)
if (idx > -1) newValue.splice(idx, 1)
}
handleChange(newValue)
}
"
/>
</FormControl>
<FormLabel
:for="`cb-${fieldName}-${item.id}`"
class="font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 md:!text-xs 2xl:text-sm"
>
{{ item.label }}
</FormLabel>
</div>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,62 @@
<script setup lang="ts">
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
items: Record<string, string>
placeholder?: string
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const { placeholder = 'Pilih', class: containerClass, selectClass, fieldGroupClass, labelClass } = props
const religionOptions = mapToComboboxOptList(props.items)
</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"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Select
:id="fieldName"
v-bind="componentField"
:items="religionOptions"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,36 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
label: string
fieldName: string
placeholder?: string
colSpan?: number
}>()
const { label, fieldName, placeholder = 'Masukkan catatan' } = props
</script>
<template>
<DE.Cell :col-span="colSpan || 1">
<DE.Label :label-for="fieldName">
{{ label }}
</DE.Label>
<DE.Field :id="fieldName">
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Textarea
v-bind="componentField"
:placeholder="placeholder"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,23 @@
<script setup lang="ts">
import BaseCheckbox from './base-checkbox.vue'
import { generalEduCode } from '~/lib/clinical.constants'
import { mapToCheckItems } from '~/lib/utils'
interface Props {
label: string
fieldName: string
colSpan?: number
}
defineProps<Props>()
const generalItems = mapToCheckItems(generalEduCode)
</script>
<template>
<BaseCheckbox
:field-name="fieldName"
:label="label"
:col-span="colSpan"
:items="generalItems"
/>
</template>
@@ -0,0 +1,22 @@
<script setup lang="ts">
import BaseCheckbox from './base-checkbox.vue'
import { specialEduCode } from '~/lib/clinical.constants'
import { mapToCheckItems } from '~/lib/utils'
interface Props {
label: string
fieldName: string
colSpan?: number
}
defineProps<Props>()
const specialItems = mapToCheckItems(specialEduCode)
</script>
<template>
<BaseCheckbox
:field-name="fieldName"
:label="label"
:col-span="colSpan"
:items="specialItems"
/>
</template>
@@ -0,0 +1,5 @@
export { default as BaseSelect } from './base-select.vue'
export { default as BaseTextarea } from './base-textarea.vue'
export { default as CheckboxGeneral } from './checkbox-general.vue'
export { default as CheckboxSpecial } from './checkbox-special.vue'
export { default as SelectAssessmentCode } from './select-assessment-code.vue'
@@ -0,0 +1,36 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
label: string
fieldName: string
placeholder?: string
colSpan?: number
}>()
const { label, fieldName, placeholder = 'Masukkan catatan' } = props
</script>
<template>
<DE.Cell :col-span="colSpan || 1">
<DE.Label :label-for="fieldName">
{{ label }}
</DE.Label>
<DE.Field :id="fieldName">
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Textarea
v-bind="componentField"
:placeholder="placeholder"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,72 @@
<script setup lang="ts">
import * as z from 'zod'
import type { ExposedForm } from '~/types/form'
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
const schema = z.object({
generalEducationNeeds: z
.array(z.string(), {
required_error: 'Mohon pilih setidaknya item',
})
.min(1, 'Mohon pilih setidaknya item'),
specificEducationNeeds: z
.array(z.string(), {
required_error: 'Mohon pilih setidaknya item',
})
.min(1, 'Mohon pilih setidaknya item'),
learningAbility: z.string({
required_error: 'Mohon pilih kemampuan belajar',
}),
learningWillingness: z.string({
required_error: 'Mohon pilih kemauan belajar',
}),
barrier: z.string({
required_error: 'Mohon pilih hambatan',
}),
learningMethod: z.string({
required_error: 'Mohon pilih metode pembelajaran',
}),
language: z.string({
required_error: 'Mohon pilih bahasa',
}),
languageBarrier: z.string({
required_error: 'Mohon pilih hambatan bahasa',
}),
beliefValue: z.string({
required_error: 'Mohon pilih keyakinan pada nilai-nilai yang dianut',
}),
})
const form = ref<ExposedForm<any> | null>(null)
async function handleActionClick(eventType: string) {
if (eventType === 'submit') {
const isValid = await form.value?.validate()
console.log(isValid)
}
if (eventType === 'cancel') {
}
}
</script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Tambah Asesmen Edukasi</div>
<AppAssessmentEducationEntryForm
ref="form"
:schema="schema"
/>
<div class="my-2 flex justify-end py-2">
<Action @click="handleActionClick" />
</div>
</template>
+87
View File
@@ -0,0 +1,87 @@
export const generalEduCode = {
'right-obg': 'Hak dan kewajiban pasien dan keluarga',
'general-consent': 'General Consent',
service: 'Pelayanan yang disediakan (jam pelayanan, akses pelayanan dan proses pelayanan)',
'all-care-service': 'Sumber alternatif asuhan di tempat lain/faskes lain',
'home-plan': 'Rencana tindakan di rumah',
'home-care': 'Kebutuhan perawatan di rumah',
orientation: 'Orientasi Ruangan',
'fall-risk-prevention': 'Pencegahan risiko jatuh',
'alt-care': 'Alternatif pelayanan',
'act-delay': 'Penundaan Tindakan',
others: 'Lain-lain',
} as const
export type GeneralEduCodeKey = keyof typeof generalEduCode
export const specialEduCode = {
'disease-diag-dev': 'Diagnosa penyakit dan perkembangannya',
'safe-med-usage': 'Penggunaan obat yang aman',
'side-effect': 'Efek samping dan reaksi obat',
diet: 'Diet/Nutrisi',
'pain-mgmt': 'Manajemen nyeri',
'medical-eq-usage': 'Penggunaan Peralatan Medis',
'rehab-technique': 'Teknik Rehabilitasi',
'prevention-act': 'Tindakan pencegahan (cuci tangan, pemasangan gelang)',
} as const
export type SpecialEduCodeKey = keyof typeof specialEduCode
export const eduAssessmentCode = {
'learn-ability': 'Kemampuan Belajar',
'learn-will': 'Kemauan Belajar',
obstacle: 'Hambatan',
'learn-method': 'Metode Pembelajaran',
lang: 'Bahasa',
'lang-obstacle': 'Hambatan Bahasa',
belief: 'Keyakinan',
} as const
export type EduAssessmentCodeKey = keyof typeof eduAssessmentCode
export const abilityCode = {
able: 'Mampu',
'not-able': 'Tidak Mampu',
} as const
export type AbilityCodeKey = keyof typeof abilityCode
export const willCode = {
ready: 'Siap',
interested: 'Tertarik',
'not-interested': 'Tidak Tertarik',
} as const
export type WillCodeKey = keyof typeof willCode
export const medObstacleCode = {
hearing: 'Pendengaran',
sight: 'Penglihatan',
physical: 'Fisik',
emotional: 'Emosional',
cognitif: 'Kognitif',
} as const
export type MedObstacleCodeKey = keyof typeof medObstacleCode
export const learnMethodCode = {
demo: 'Demonstrasi',
'discuss-leaflet': 'Diskusi Leaflet',
} as const
export type LearnMethodCodeKey = keyof typeof learnMethodCode
export const langClassCode = {
ind: 'Indonesia',
region: 'Daerah',
foreign: 'Asing',
} as const
export type LangClassCodeKey = keyof typeof langClassCode
export const translatorSrcCode = {
team: 'Tim Penerjemah',
family: 'Keluarga',
} as const
export type TranslatorSrcCodeKey = keyof typeof translatorSrcCode
+13 -1
View File
@@ -7,6 +7,10 @@ export interface SelectOptionType<_T = string> {
label: string
code?: string
}
export interface CheckItem {
id: string
label: string
}
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -27,6 +31,15 @@ export function mapToComboboxOptList(items: Record<string, string>): SelectOptio
return result
}
export function mapToCheckItems<T extends Record<string, string>, K extends keyof T & string>(
items: T,
): { id: K; label: T[K] }[] {
return Object.entries(items).map(([key, value]) => ({
id: key as K,
label: value as T[K],
}))
}
/**
* Mengkonversi string menjadi title case (huruf pertama setiap kata kapital)
* @param str - String yang akan dikonversi
@@ -36,7 +49,6 @@ export function toTitleCase(str: string): string {
return str.toLowerCase().replace(/\b\w/g, (char) => char.toUpperCase())
}
/**
* Menghitung umur berdasarkan tanggal lahir
* @param birthDate - Tanggal lahir dalam format Date atau string
@@ -0,0 +1,43 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah Assessment Education',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
// const hasAccess = checkRole(roleAccess)
const hasAccess = true
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
</script>
<template>
<div>
<div v-if="canRead">
<ContentAssessmentEducationAdd />
</div>
<Error
v-else
:status-code="403"
/>
</div>
</template>