Merge pull request #222 from dikstub-rssa/feat/education-assessment-79

Feat/education assessment 79
This commit is contained in:
Munawwirul Jamal
2025-12-08 12:25:42 +07:00
committed by GitHub
31 changed files with 1591 additions and 110 deletions
@@ -0,0 +1,215 @@
<script setup lang="ts">
import { z } from 'zod'
import { FieldArray, useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
// types
// import { type AssessmentEducationFormData, AssessmentEducationSchema, encode } from '~/schemas/assessment-education'
// componenets
import * as DE from '~/components/pub/my-ui/doc-entry'
import Separator from '~/components/pub/ui/separator/Separator.vue'
import { BaseSelect, CheckboxGeneral, CheckboxSpecial } from './fields'
import { ButtonAction, TextAreaInput } from '~/components/pub/my-ui/form'
import ActionForm from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
// constant
import {
abilityCode,
willCode,
medObstacleCode,
learnMethodCode,
langClassCode,
translatorSrcCode,
} from '~/lib/clinical.constants'
interface FormData {}
interface Props {
noteLimit?: number
isLoading?: boolean
isReadonly?: boolean
mode: 'add' | 'edit' | 'view'
initialValues?: Partial<FormData>
}
const props = withDefaults(defineProps<Props>(), {
isLoading: false,
isReadonly: false,
noteLimit: 10,
})
const isDisabled = computed(() => props.isLoading || props.isReadonly)
// const formSchema = toTypedSchema(AssessmentEducationSchema)
const formSchema = toTypedSchema(z.object({}))
const { errors, handleSubmit, values, meta, resetForm, setFieldValue, setValues, validate } = useForm<FormData>({
name: 'assessmentEducationForm',
validationSchema: formSchema,
initialValues: props.initialValues
? props.initialValues
: {
plans: [
{
id: 1,
value: '',
},
],
},
validateOnMount: false,
})
defineExpose({
validate,
resetForm,
setValues,
values,
})
</script>
<template>
<form @submit.prevent="">
<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"
:is-disabled="isDisabled"
/>
<CheckboxSpecial
field-name="specificEducationNeeds"
label="Edukasi Khusus"
:col-span="2"
:is-disabled="isDisabled"
/>
</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"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="learningWillingness"
label="Kemauan Belajar"
:items="willCode"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="barrier"
label="Hambatan"
:items="medObstacleCode"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="learningMethod"
label="Metode Pembelajaran"
:items="learnMethodCode"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="language"
label="Bahasa"
:items="langClassCode"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="languageBarrier"
label="Hambatan Bahasa"
:items="translatorSrcCode"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="beliefValue"
label="Keyakinan pada Nilai-Nilai yang Dianut"
:items="{
ya: 'IYA',
tidak: 'TIDAK',
}"
:is-disabled="isDisabled"
/>
</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
:col-count="1"
:cell-flex="false"
>
<FieldArray
v-slot="{ fields, push, remove }"
name="tissueNotes"
>
<template v-if="fields.length === 0">
{{ push({ note: '' }) }}
</template>
<div class="space-y-4">
<DE.Block
v-for="(field, idx) in fields"
:key="field.key"
:col-count="6"
:cell-flex="false"
>
<DE.Cell :col-span="5">
<TextAreaInput
:field-name="`plans[${idx}].value`"
:label="'Rencana ' + Number(idx + 1)"
placeholder="Masukkan rencana catatan"
:col-span="2"
:rows="5"
:is-disabled="isReadonly"
/>
</DE.Cell>
<DE.Cell class="flex items-start justify-start">
<DE.Field :class="idx === 0 ? 'mt-[30px]' : 'mt-0'">
<ButtonAction
v-if="idx !== 0"
:disabled="isReadonly"
preset="delete"
:title="`Hapus Rencana ${idx + 1}`"
icon-only
@click="remove(idx)"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
</div>
<div class="self-center pt-3">
<ButtonAction
preset="add"
label="Tambah Rencana"
title="Tambah Rencana Edukasi"
:disabled="fields.length >= noteLimit || isReadonly"
:full-width-mobile="true"
class="mt-4"
@click="push({ id: fields.length + 1, value: '' })"
/>
</div>
</FieldArray>
</DE.Block>
<!-- todo -->
<div class="mt-4 flex justify-end">
<ActionForm @click="() => {}" />
</div>
</form>
</template>
@@ -0,0 +1,66 @@
<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[]
isDisabled?: boolean
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
:disabled="isDisabled"
: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,79 @@
<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 opts = computed(() => {
const data = mapToComboboxOptList(props.items)
// hide code on select options
const filtered = data
.filter((item) => item.code)
.reduce(
(acc, item) => {
acc.push({
label: item.label as string,
value: item.value as string,
})
return acc
},
[] as Array<{ label: string; value: string }>,
)
return filtered
})
</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="opts"
: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,25 @@
<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
isDisabled?: boolean
colSpan?: number
}
defineProps<Props>()
const generalItems = mapToCheckItems(generalEduCode)
</script>
<template>
<BaseCheckbox
:field-name="fieldName"
:label="label"
:is-disabled="isDisabled"
: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,4 @@
export { default as BaseSelect } from './base-select.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,75 @@
import { defineAsyncComponent } from 'vue'
import { format } from 'date-fns'
import { id } from 'date-fns/locale'
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import type { ActionReportData } from '~/components/app/action-report/sample'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [{ width: 120 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 50 }],
headers: [
[
{ label: 'TANGGAL' },
{ label: 'INFORMASI UMUM' },
{ label: 'EDUKASI KHUSUS' },
{ label: 'RENCANA EDUKASI' },
{ label: 'PELAKSANAAN' },
{ label: 'AKSI' },
],
],
keys: ['reportAt', 'dpjp', 'operator', 'operationAt', 'operationType', 'action'],
delKeyNames: [
{ key: 'id', label: 'ID' },
{ key: 'dokter', label: 'Dokter' },
{ key: 'reportAt', label: 'Tanggal Laporan' },
],
parses: {
reportAt: (rec: unknown): unknown => {
const attr = (rec as ActionReportData).reportAt
const result = format(new Date(attr), 'd MMMM yyyy, HH:mm', { locale: id })
return result
},
operationAt: (rec: unknown): unknown => {
return '1 Rencana Edukasi'
},
system: (rec: unknown): unknown => {
return 'Cito'
},
operator: (rec: unknown): unknown => {
return '2 Edukasi dipilih'
},
billing: (rec: unknown): unknown => {
return 'General'
},
operationType: (rec: unknown): unknown => {
return '-'
},
dpjp: (rec: unknown): unknown => {
return '3 Informasi Dipilih'
},
parent: (rec: unknown): unknown => {
const recX = rec as any
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
@@ -0,0 +1,39 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
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,75 @@
import { defineAsyncComponent } from 'vue'
import { format } from 'date-fns'
import { id } from 'date-fns/locale'
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import type { ActionReportData } from '~/components/app/action-report/sample'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [{ width: 120 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 50 }],
headers: [
[
{ label: 'TANGGAL' },
{ label: 'INFORMASI UMUM' },
{ label: 'EDUKASI KHUSUS' },
{ label: 'RENCANA EDUKASI' },
{ label: 'PELAKSANAAN' },
{ label: 'AKSI' },
],
],
keys: ['reportAt', 'dpjp', 'operator', 'operationAt', 'operationType', 'action'],
delKeyNames: [
{ key: 'id', label: 'ID' },
{ key: 'dokter', label: 'Dokter' },
{ key: 'reportAt', label: 'Tanggal Laporan' },
],
parses: {
reportAt: (rec: unknown): unknown => {
const attr = (rec as ActionReportData).reportAt
const result = format(new Date(attr), 'd MMMM yyyy, HH:mm', { locale: id })
return result
},
operationAt: (rec: unknown): unknown => {
return '1 Rencana Edukasi'
},
system: (rec: unknown): unknown => {
return 'Cito'
},
operator: (rec: unknown): unknown => {
return '2 Edukasi dipilih'
},
billing: (rec: unknown): unknown => {
return 'General'
},
operationType: (rec: unknown): unknown => {
return '-'
},
dpjp: (rec: unknown): unknown => {
return '3 Informasi Dipilih'
},
parent: (rec: unknown): unknown => {
const recX = rec as any
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
@@ -0,0 +1,39 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
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,54 @@
import { addWeeks, formatISO } from 'date-fns'
export type ActionReportData = {
id: number
reportAt: string
operationAt: string
noRm: string
noBill: string
nama: string
jk: string
alamat: string
klinik: string
dokter: string
caraBayar: string
rujukan: string
ketRujukan: string
asal: string
}
export const sampleRows: ActionReportData[] = [
{
id: 1,
reportAt: formatISO(addWeeks(new Date(), -1)),
operationAt: formatISO(addWeeks(new Date(), 1)),
noRm: 'RM23311224',
noBill: '-',
nama: 'Ahmad Baidowi',
jk: 'L',
alamat: 'Jl Jaksa Agung S. No. 9',
klinik: 'Penyakit dalam',
dokter: 'Dr. Andreas Sutaji',
caraBayar: 'JKN',
rujukan: 'Faskes BPJS',
ketRujukan: 'RUMAH SAKIT - RS Lawang Medika - Malang',
asal: 'Rawat Jalan Reguler',
},
{
id: 2,
reportAt: new Date().toISOString(),
operationAt: formatISO(addWeeks(new Date(), 2)),
noRm: 'RM23455667',
noBill: '-',
nama: 'Abraham Sulaiman',
jk: 'L',
alamat: 'Purwantoro, Blimbing',
klinik: 'Penyakit dalam',
dokter: 'Dr. Andreas Sutaji',
caraBayar: 'JKN',
rujukan: 'Faskes BPJS',
ketRujukan: 'RUMAH SAKIT - RS Lawang Medika - Malang',
asal: 'Rawat Jalan Reguler',
},
// tambahkan lebih banyak baris contoh jika perlu
]
@@ -39,7 +39,7 @@ defineExpose({
<div>
<p
v-if="props.title"
class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold"
class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base"
>
{{ props.title || 'Kontak Pasien' }}
</p>
@@ -0,0 +1,34 @@
<script setup lang="ts">
import List from './list.vue'
import Form from './form.vue'
import View from './view.vue'
// Models
import type { Encounter } from '~/models/encounter'
// Props
interface Props {
encounter: Encounter
}
const props = defineProps<Props>()
const { mode, goToEntry, goToView } = useQueryCRUDMode('mode')
</script>
<template>
<div>
<List
v-if="mode === 'list'"
:encounter="props.encounter"
@add="goToEntry"
@edit="goToEntry({ fromView: false })"
@view="goToView"
/>
<View
v-else-if="mode === 'view'"
:encounter="props.encounter"
/>
<Form v-else />
</div>
</template>
@@ -0,0 +1,74 @@
<script setup lang="ts">
import mockData from './sample'
// type
import { genDoctor, type Doctor } from '~/models/doctor'
import type { ActionReportFormData } from '~/schemas/action-report.schema'
// components
import { toast } from '~/components/pub/ui/toast'
import AppAssessmentEducationEntry from '~/components/app/assessment-education/entry.vue'
// states
const route = useRoute()
const { mode, goBack } = useQueryCRUDMode('mode')
const { recordId } = useQueryCRUDRecordId('record-id')
const reportData = ref<ActionReportFormData>({} as unknown as ActionReportFormData)
const doctors = ref<Doctor[]>([])
const isLoading = ref<boolean>(false)
// TODO: dummy data
;(() => {
doctors.value = [genDoctor()]
})()
const entryMode = ref<'add' | 'edit' | 'view'>('add')
const isDataReady = ref(false)
onMounted(async () => {
if (mode.value === 'entry' && recordId.value) {
entryMode.value = 'edit'
await loadEntryForEdit(+recordId.value)
} else {
// Untuk mode 'add', langsung set ready
isDataReady.value = true
}
})
// TODO: map data
async function loadEntryForEdit(id: number | string) {
isLoading.value = true
const result = mockData
reportData.value = result as ActionReportFormData
isLoading.value = false
isDataReady.value = true
}
</script>
<template>
<AppAssessmentEducationEntry
v-if="isDataReady"
:isLoading="isLoading"
:mode="entryMode"
@submit="(val: {}) => console.log(val)"
@back="goBack"
@error="
(err: Error) => {
toast({
title: 'Terjadi Kesalahan',
description: err.message,
variant: 'destructive',
})
}
"
:doctors="doctors"
:initialValues="reportData"
/>
<div
v-else
class="flex items-center justify-center p-8"
>
<p class="text-muted-foreground">Memuat data...</p>
</div>
</template>
@@ -0,0 +1,276 @@
<script setup lang="ts">
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import AppAssessmentEducationList from '~/components/app/assessment-education/list.vue'
import AppAssessmentEducationListHistory from '~/components/app/assessment-education/list-history.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ButtonAction } from '~/components/pub/my-ui/form'
// config
import { config } from '~/components/app/assessment-education/list.cfg'
// types
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import type { Encounter } from '~/models/encounter'
// Samples
import { sampleRows, type AssessmentEducationData } from '~/components/app/assessment-education/sample'
import sampleReport from './sample'
// helpers
import { toast } from '~/components/pub/ui/toast'
// Props
interface Props {
encounter: Encounter
}
const props = defineProps<Props>()
const emits = defineEmits<{
(e: 'add'): void
(e: 'edit', id: number | string): void
(e: 'view', id: number | string): void
}>()
// states
const router = useRouter()
const route = useRoute()
const { goToEntry, backToList } = useQueryCRUDMode('mode')
const title = ref('')
const search = ref('')
const dateFrom = ref('')
const dateTo = ref('')
const isDialogOpen = ref<boolean>(false)
const isLoading = ref<boolean>(false)
// #region mock
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/consultation.handler'
// #endregion
// filter + pencarian sederhana (client-side)
const filtered = computed(() => {
const q = search.value.trim().toLowerCase()
return sampleRows.filter((r: ActionReportData) => {
if (q) {
return r.nama.toLowerCase().includes(q) || r.noRm.toLowerCase().includes(q) || r.dokter.toLowerCase().includes(q)
}
return true
})
})
const goEdit = (id: number | string) => {
router.replace({
path: route.path,
query: {
...route.query,
mode: 'entry',
'record-id': id,
},
})
}
const goView = (id: number | string) => {
router.replace({
path: route.path,
query: {
...route.query,
mode: 'view',
'record-id': id,
},
})
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
async function onGetDetail(id: number | string) {
isLoading.value = true
const res = sampleReport
recItem.value = res
console.log(res)
isLoading.value = false
}
// #region watcher
watch([recId, recAction], (newVal) => {
const [id, action] = newVal
// Guard: jangan proses jika id = 0 atau action kosong
if (!id || !action) return
switch (action) {
case ActionEvents.showDetail:
// onGetDetail(recId.value)
goView(id)
title.value = 'Detail Konsultasi'
break
case ActionEvents.showEdit:
goEdit(id)
title.value = 'Edit Konsultasi'
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
// Reset KEDUANYA menggunakan nextTick agar tidak trigger watcher lagi
nextTick(() => {
recId.value = 0
recAction.value = ''
})
})
// #endregion
</script>
<template>
<div class="mx-auto max-w-full">
<div class="border-b p-6">
<h1 class="text-2xl font-semibold">Asesmen Kebutuhan Edukasi</h1>
<p class="mt-1 text-sm text-gray-500">Manajemen asesmen kebutuhan edukasi pasien rawat jalan</p>
</div>
<div class="flex flex-wrap items-center gap-3 border-b p-4">
<div class="flex items-center gap-2">
<input
v-model="search"
placeholder="Cari Nama / No.RM"
class="w-64 rounded border px-3 py-2"
/>
</div>
<div class="flex items-center gap-2">
<input
v-model="dateFrom"
type="date"
class="rounded border px-3 py-2"
/>
<span class="text-sm text-gray-500">-</span>
<input
v-model="dateTo"
type="date"
class="rounded border px-3 py-2"
/>
<ButtonAction
preset="custom"
title="Filter List Asesmen"
label="Filter"
icon="i-lucide-filter"
@click="
() => {
isDialogOpen = true
}
"
/>
</div>
<div class="ml-auto flex items-center gap-2">
<ButtonAction
preset="custom"
title="Riwayat"
icon="i-lucide-history"
label="Riwayat"
@click="
() => {
isDialogOpen = true
}
"
/>
<ButtonAction
preset="add"
title="Tambah Asesmen"
icon="i-lucide-plus"
label="Asesmen"
@click="
() => {
goToEntry()
}
"
/>
</div>
</div>
<div class="overflow-x-auto p-4">
<AppAssessmentEducationList
:data="filtered"
:pagination-meta="{
recordCount: 2,
page: 1,
pageSize: 10,
totalPage: 1,
hasPrev: false,
hasNext: false,
}"
/>
</div>
</div>
<Dialog
v-model:open="isDialogOpen"
title="Arsip Riwayat Asesmen Kebutuhan Edukasi"
size="2xl"
prevent-outside
@update:open="
(value: any) => {
isDialogOpen = value
}
"
>
<AppAssessmentEducationListHistory
:data="filtered"
:pagination-meta="{
recordCount: 2,
page: 1,
pageSize: 10,
totalPage: 1,
hasPrev: false,
hasNext: false,
}"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="
() =>
handleActionRemove(
recItem.id,
() => {
router.go(0)
},
toast,
)
"
@cancel=""
>
<template #default="{ record }">
{{ console.log(JSON.stringify(record)) }}
<div class="space-y-1 text-sm">
<p
v-for="field in config.delKeyNames"
:key="field.key"
:v-if="record?.[field.key]"
>
<span class="font-semibold">{{ field.label }}:</span>
{{ record[field.key] }}
</p>
</div>
</template>
</RecordConfirmation>
</template>
@@ -0,0 +1,68 @@
export default {
operatorTeam: {
dpjpId: -1,
operatorName: 'Julian Alvarez',
assistantOperatorName: 'Arda Guller',
instrumentNurseName: 'Kenan Yildiz',
surgeryDate: '2025-11-13T14:29:00',
actionDiagnosis: 'Sprei gratisnya mana',
},
procedures: [
{
id: -1,
name: 'Ndase mumet',
code: 'CX1',
},
],
operationExecution: {
surgeryType: 'khusus',
billingCode: 'local',
operationSystem: 'cito',
surgeryCleanType: 'kotor',
surgeryNumber: 'retry',
birthPlaceNote: 'out3',
personWeight: 100,
operationDescription: 'asdsadsa1',
birthRemark: 'lahir_hidup',
operationStartAt: '2025-11-13T14:29:00',
operationEndAt: '2025-11-13T17:29:00',
anesthesiaStartAt: '2025-11-13T11:29:00',
anesthesiaEndAt: '2025-11-13T18:29:00',
},
bloodInput: {
type: 'tc',
amount: {
prc: null,
wb: null,
ffp: null,
tc: 3243324,
},
},
implant: {
brand: 'Samsung',
name: 'S.Komedi',
companionName: 'When ya',
},
specimen: {
destination: 'pa',
},
tissueNotes: [
{
note: 'Anjai',
},
{
note: 'Ciee Kaget',
},
{
note: 'Baper',
},
{
note: 'Saltink weeh',
},
{
note: 'Kaburrr',
},
],
}
@@ -0,0 +1,67 @@
<script setup lang="ts">
import mockData from './sample'
// types
import { type ActionReportFormData } from '~/schemas/action-report.schema'
import { type Encounter } from '~/models/encounter'
// Components
import AppActionReportPreview from '~/components/app/action-report/preview.vue'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
// #region Props & Emits
const router = useRouter()
const { backToList, goToEntry } = useQueryCRUDMode('mode')
const { recordId } = useQueryCRUDRecordId('record-id')
function onEditFromView() {
goToEntry({ fromView: true })
}
const props = defineProps<{
encounter: Encounter
}>()
// #endregion
// #region State & Computed
const reportData = ref<ActionReportFormData | null>(null)
const headerPrep: HeaderPrep = {
title: 'Detail Laporan Tindakan',
icon: 'i-lucide-stethoscope',
}
// #endregion
// #region Lifecycle Hooks
onMounted(async () => {
reportData.value = mockData as unknown as ActionReportFormData
})
// #endregion
// #region Functions
// #endregion region
// #region Utilities & event handlers
function onEdit() {
router.push({
name: 'action-report-id-edit',
params: { id: 100 },
})
}
function onBack() {}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<AppActionReportPreview
v-if="reportData"
:data="reportData"
@back="backToList"
@edit="onEditFromView"
/>
</template>
@@ -1,33 +1,40 @@
<script setup lang="ts">
const props = defineProps<{
height?: number
class?: string
activeTab?: 1 | 2
}>()
const classVal = computed(() => {
return props.class ? props.class : ''
})
const activeTab = ref(props.activeTab || 1)
function switchActiveTab() {
activeTab.value = activeTab.value === 1 ? 2 : 1
function handleClick(value: 1 | 2) {
activeTab.value = value
}
</script>
<template>
<div :class="`content-switcher ${classVal}`" :style="height ? `height:${200}px` : ''">
<div class="wrapper">
<div :class="`item item-1 ${activeTab === 1 ? 'active' : 'inactive'}`">
<slot name="content1" />
<div class="content-switcher" :style="`height: ${height || 200}px`">
<div :class="`${activeTab === 1 ? 'active' : 'inactive'}`">
<div class="content-wrapper">
<div>
<slot name="content1" />
</div>
</div>
<div :class="`nav border-slate-300 ${ activeTab == 1 ? 'border-l' : 'border-r'}`">
<button @click="switchActiveTab()" class="!p-0 w-full h-full">
<Icon :name="activeTab == 1 ? 'i-lucide-chevron-left' : 'i-lucide-chevron-right'" class="text-3xl" />
<div class="content-nav">
<button @click="handleClick(1)">
<Icon name="i-lucide-chevron-right" />
</button>
</div>
<div :class="`item item-2 ${activeTab === 2 ? 'active' : 'inactive'}`">
<slot name="content2" />
</div>
<div :class="`${activeTab === 2 ? 'active' : 'inactive'}`">
<div class="content-nav">
<button @click="handleClick(2)">
<Icon name="i-lucide-chevron-left" />
</button>
</div>
<div class="content-wrapper">
<div>
<slot name="content2" />
</div>
</div>
</div>
</div>
@@ -35,24 +42,45 @@ function switchActiveTab() {
<style>
.content-switcher {
@apply overflow-hidden
@apply flex overflow-hidden gap-3
}
.wrapper {
@apply flex w-[200%] h-full
}
.item {
@apply w-[calc(50%-60px)]
.content-switcher > * {
@apply border border-slate-300 rounded-md flex overflow-hidden
}
.item-1.active {
@apply ms-0 transition-all duration-500 ease-in-out
.content-wrapper {
@apply p-4 2xl:p-5 overflow-hidden grow
}
.item-1.inactive {
@apply -ms-[calc(50%-60px)] transition-all duration-500 ease-in-out
.inactive .content-wrapper {
@apply p-0 w-0
}
.nav {
@apply h-full w-[60px] flex flex-row items-center justify-center content-center !text-2xl overflow-hidden
.content-nav {
@apply h-full flex flex-row items-center justify-center content-center !text-2xl overflow-hidden
}
.content-nav button {
@apply pt-2 px-2 h-full w-full
}
/* .content-switcher .inactive > .content-wrapper {
@apply w-0 p-0 opacity-0 transition-all duration-500 ease-in-out
} */
.content-switcher .inactive {
@apply w-16 transition-all duration-500 ease-in-out
}
.content-switcher .inactive > .content-nav {
@apply w-full transition-all duration-100 ease-in-out
}
.content-switcher .active {
@apply grow transition-all duration-500 ease-in-out
}
.content-switcher .active > .content-nav {
@apply w-0 transition-all duration-100 ease-in-out
}
/* .content-switcher .active > .content-wrapper {
@apply w-full delay-1000 transition-all duration-1000 ease-in-out
} */
</style>
+2 -2
View File
@@ -53,8 +53,8 @@ const settingClass = computed(() => {
'[&_.cell]:2xl:flex',
][getBreakpointIdx(props.cellFlexPoint)]
cls += ' [&_.label]:flex-shrink-0 ' + [
'[&_.label]:md:w-16 [&_.label]:xl:w-20',
'[&_.label]:md:w-20 [&_.label]:xl:w-24',
'[&_.label]:md:w-12 [&_.label]:xl:w-20',
'[&_.label]:md:w-16 [&_.label]:xl:w-24',
'[&_.label]:md:w-24 [&_.label]:xl:w-32',
'[&_.label]:md:w-32 [&_.label]:xl:w-40',
'[&_.label]:md:w-44 [&_.label]:xl:w-52',
+1 -10
View File
@@ -4,10 +4,6 @@ import { type EncounterItem } from "~/handlers/encounter-init.handler";
const props = defineProps<{
initialActiveMenu: string
data: EncounterItem[]
canCreate?: boolean
canRead?: boolean
canUpdate?: boolean
canDelete?: boolean
}>()
const activeMenu = ref(props.initialActiveMenu)
@@ -42,12 +38,7 @@ function changeMenu(value: string) {
class="flex-1 rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
<component
:is="data.find((m) => m.id === activeMenu)?.component"
v-bind="data.find((m) => m.id === activeMenu)?.props"
:can-create="canCreate"
:can-read="canRead"
:can-update="canUpdate"
:can-delete="canDelete"
/>
v-bind="data.find((m) => m.id === activeMenu)?.props" />
</div>
</div>
</div>
+6 -8
View File
@@ -30,17 +30,15 @@ export function useQueryCRUD(modeKey: string = 'mode', recordIdKey: string = 're
})
const goToEntry = (myRecord_id?: any) => {
if(myRecord_id) {
crudQueryParams.value = { mode: 'entry', recordId: myRecord_id }
if (myRecord_id) {
crudQueryParams.value.mode = 'entry'
crudQueryParams.value.recordId = myRecord_id
} else {
crudQueryParams.value = { mode: 'entry', recordId: undefined }
crudQueryParams.value.mode = 'entry'
crudQueryParams.value.recordId = undefined
}
}
const goToDetail = (myRecord_id: string|number) => {
crudQueryParams.value = { mode: 'list', recordId: String(myRecord_id) }
}
const backToList = () => {
delete route.query[recordIdKey]
router.push({
@@ -52,7 +50,7 @@ export function useQueryCRUD(modeKey: string = 'mode', recordIdKey: string = 're
})
}
return { crudQueryParams, goToEntry, goToDetail, backToList }
return { crudQueryParams, goToEntry, backToList }
}
export function useQueryCRUDMode(key: string = 'mode') {
@@ -0,0 +1,24 @@
// Handlers
import { genCrudHandler } from '~/handlers/_handler'
// Services
import { create, update, remove } from '~/services/assessment-education.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
+51 -55
View File
@@ -46,15 +46,18 @@ const ApLabOrderAsync = defineAsyncComponent(() => import('~/components/content/
const CprjAsync = defineAsyncComponent(() => import('~/components/content/cprj/entry.vue'))
const RadiologyAsync = defineAsyncComponent(() => import('~/components/content/radiology-order/main.vue'))
const ConsultationAsync = defineAsyncComponent(() => import('~/components/content/consultation/list.vue'))
const DocUploadListAsync = defineAsyncComponent(() => import('~/components/content/document-upload/main.vue'))
const DocUploadListAsync = defineAsyncComponent(() => import('~/components/content/document-upload/list.vue'))
const GeneralConsentListAsync = defineAsyncComponent(() => import('~/components/content/general-consent/entry.vue'))
const ResumeListAsync = defineAsyncComponent(() => import('~/components/content/resume/main.vue'))
const ControlLetterListAsync = defineAsyncComponent(() => import('~/components/content/control-letter/main.vue'))
const KfrListAsync = defineAsyncComponent(() => import('~/components/content/kfr/main.vue'))
const PrbListAsync = defineAsyncComponent(() => import('~/components/content/prb/main.vue'))
const SurgeryReportListAsync = defineAsyncComponent(() => import('~/components/content/surgery-report/main.vue'))
const VaccineDataListAsync = defineAsyncComponent(() => import('~/components/content/vaccine-data/main.vue'))
const ResumeListAsync = defineAsyncComponent(() => import('~/components/content/resume/list.vue'))
const ControlLetterListAsync = defineAsyncComponent(() => import('~/components/content/control-letter/list.vue'))
const KfrListAsync = defineAsyncComponent(() => import('~/components/content/kfr/list.vue'))
const PrbListAsync = defineAsyncComponent(() => import('~/components/content/prb/list.vue'))
const SurgeryReportListAsync = defineAsyncComponent(() => import('~/components/content/surgery-report/list.vue'))
const VaccineDataListAsync = defineAsyncComponent(() => import('~/components/content/vaccine-data/list.vue'))
const InitialNursingStudyAsync = defineAsyncComponent(() => import('~/components/content/initial-nursing/entry.vue'))
const AssessmentEducationEntryAsync = defineAsyncComponent(
() => import('~/components/content/assessment-education/entry.vue'),
)
const SummaryMedicAsync = defineAsyncComponent(() => import('~/components/content/summary-medic/entry.vue'))
const ActionReportEntryAsync = defineAsyncComponent(() => import('~/components/content/action-report/entry.vue'))
@@ -65,19 +68,17 @@ const defaultKeys: Record<string, any> = {
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
// NOTE : HIDDEN UNTIL IT IS READY
// earlyNurseryAssessment: {
// id: 'early-nursery-assessment',
// title: 'Pengkajian Awal Keperawatan',
// classCode: ['ambulatory', 'emergency', 'inpatient'],
// unit: 'all',
// },
earlyNurseryAssessment: {
id: 'early-nursery-assessment',
title: 'Pengkajian Awal Keperawatan',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
earlyMedicalAssessment: {
id: 'early-medical-assessment',
title: 'Pengkajian Awal Medis',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
afterId: 'early-medical-assessment',
},
earlyMedicalRehabAssessment: {
id: 'rehab-medical-assessment',
@@ -86,27 +87,20 @@ const defaultKeys: Record<string, any> = {
unit: 'rehab',
afterId: 'early-medical-assessment',
},
fkr: {
id: 'fkr',
title: 'FKR',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
functionAssessment: {
id: 'function-assessment',
title: 'Asesmen Fungsi',
classCode: ['ambulatory'],
unit: 'rehab',
afterId: 'rehab-medical-assessment',
},
therapyProtocol: {
id: 'therapy-protocol',
classCode: ['ambulatory'],
title: 'Protokol Terapi',
unit: 'rehab',
afterId: 'function-assessment',
},
// NOTE: Replaced by FRK
// functionAssessment: {
// id: 'function-assessment',
// title: 'Asesmen Fungsi',
// classCode: ['ambulatory'],
// unit: 'rehab',
// afterId: 'rehab-medical-assessment',
// },
// therapyProtocol: {
// id: 'therapy-protocol',
// classCode: ['ambulatory'],
// title: 'Protokol Terapi',
// unit: 'rehab',
// afterId: 'function-assessment',
// },
chemotherapyProtocol: {
id: 'chemotherapy-protocol',
title: 'Protokol Kemoterapi',
@@ -229,6 +223,12 @@ const defaultKeys: Record<string, any> = {
classCode: ['ambulatory', 'emergency'],
unit: 'all',
},
kfr: {
id: 'kfr',
title: 'KFR',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
refBack: {
id: 'reference-back',
title: 'PRB',
@@ -265,12 +265,6 @@ const defaultKeys: Record<string, any> = {
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
summaryMedic: {
id: 'summary-medic',
title: 'Profil Ringkasan Medis',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
initialNursingStudy: {
id: 'initial-nursing-study',
title: 'Kajian Awal Keperawatan',
@@ -362,9 +356,12 @@ export function injectComponents(id: string | number, data: EncounterListData, m
}
}
if (currentKeys?.educationAssessment) {
// TODO: add component for education assessment
currentKeys.educationAssessment['component'] = null
currentKeys.educationAssessment['props'] = { encounter_id: id }
currentKeys.educationAssessment['component'] = AssessmentEducationEntryAsync
currentKeys.educationAssessment['props'] = {
encounter: data?.encounter,
type: 'education-assessment',
label: currentKeys.educationAssessment['title'],
}
}
if (currentKeys?.generalConsent) {
currentKeys.generalConsent['component'] = GeneralConsentListAsync
@@ -424,9 +421,9 @@ export function injectComponents(id: string | number, data: EncounterListData, m
currentKeys.refBack['component'] = PrbListAsync
currentKeys.refBack['props'] = { encounter: data?.encounter }
}
if (currentKeys?.fkr) {
currentKeys.fkr['component'] = KfrListAsync
currentKeys.fkr['props'] = { encounter: data?.encounter }
if (currentKeys?.kfr) {
currentKeys.kfr['component'] = KfrListAsync
currentKeys.kfr['props'] = { encounter: data?.encounter }
}
if (currentKeys?.screening) {
// TODO: add component for screening
@@ -450,10 +447,12 @@ export function injectComponents(id: string | number, data: EncounterListData, m
currentKeys.priceList['component'] = null
currentKeys.priceList['props'] = { encounter_id: id }
}
if (currentKeys?.summaryMedic) {
currentKeys.summaryMedic['component'] = SummaryMedicAsync
currentKeys.summaryMedic['props'] = { encounter_id: id }
if (currentKeys?.initialNursingStudy) {
currentKeys.initialNursingStudy['component'] = InitialNursingStudyAsync
currentKeys.initialNursingStudy['props'] = { encounter: data?.encounter }
}
if (currentKeys?.initialNursingStudy) {
currentKeys.initialNursingStudy['component'] = InitialNursingStudyAsync
currentKeys.initialNursingStudy['props'] = { encounter: data?.encounter }
@@ -527,12 +526,9 @@ export function mapResponseToEncounter(result: any): any {
? result.visitDate
: result.registeredAt || result.patient?.registeredAt || null,
adm_employee_id: result.adm_employee_id || 0,
adm_employee: result.adm_employee || null,
responsible_nurse_code: result.responsible_nurse_code || null,
responsible_nurse: result.responsible_nurse || null,
appointment_doctor_code: result.appointment_doctor_code || null,
appointment_doctor_id: result.appointment_doctor_id || null,
responsible_doctor_id: result.responsible_doctor_id || null,
appointment_doctor: result.appointment_doctor || null,
responsible_doctor_code: result.responsible_doctor_id || null,
responsible_doctor: result.responsible_doctor || null,
refSource_name: result.refSource_name || null,
appointment_id: result.appointment_id || null,
+116
View File
@@ -0,0 +1,116 @@
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
// helpers
type EduCodeType = 'general' | 'special'
export function serializeKeyToBoolean(type: EduCodeType, selected: string[]): Record<string, boolean> {
switch (type) {
case 'general': {
return Object.keys(generalEduCode).reduce(
(acc, key) => {
acc[key] = selected.includes(key)
return acc
},
{} as Record<string, boolean>,
)
}
case 'special': {
return Object.keys(specialEduCode).reduce(
(acc, key) => {
acc[key] = selected.includes(key)
return acc
},
{} as Record<string, boolean>,
)
}
default:
throw new Error('unknown type to serialize')
}
}
+13 -1
View File
@@ -9,6 +9,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))
@@ -29,6 +33,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
@@ -38,7 +51,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
+1 -1
View File
@@ -52,7 +52,7 @@ export function genEncounter(): Encounter {
patient: genPatient(),
registeredAt: '',
class_code: '',
unit_code: '',
unit_code: 0,
unit: genUnit(),
visitDate: '',
adm_employee_id: 0,
-2
View File
@@ -1,9 +1,7 @@
import { type Base, genBase } from "./_base"
import type { Employee } from "./employee"
export interface Nurse extends Base {
employee_id: number
employee?: Employee
ihs_number?: string
unit_id: number
infra_id: number
@@ -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>
+2 -3
View File
@@ -4,9 +4,8 @@ import { InternalReferenceSchema } from './internal-reference.schema'
// Check In
const CheckInSchema = z.object({
// registeredAt: z.string({ required_error: 'Tanggal masuk harus diisi' }),
responsible_doctor_code: z.string({ required_error: 'Dokter harus diisi' }),
// adm_employee_id: z.number({ required_error: 'PJA harus diisi' }).gt(0, 'PJA harus diisi'),
registeredAt: z.string({ required_error: 'waktu harus diisi' }),
responsible_doctor_id: z.number({ required_error: 'Dokter harus diisi' }).gt(0, 'Dokter harus diisi'),
adm_employee_id: z.number({ required_error: 'PJA harus diisi' }).gt(0, 'PJA harus diisi'),
})
type CheckInFormData = z.infer<typeof CheckInSchema>
@@ -0,0 +1,28 @@
// Base
import * as base from './_crud-base'
// Types
import type { EduAssessmentRaw, EduAssessment } from '~/models/edu-assessment'
const path = '/api/v1/edu-assessment'
const name = 'edu-assessment'
export function create(data: EduAssessmentRaw) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
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)
}