feat/consultation-82: done

This commit is contained in:
2025-10-05 03:30:22 +07:00
parent ce785f2092
commit 427d97c29f
10 changed files with 554 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
<script setup lang="ts">
// Components
import * as DE from '~/components/pub/custom-ui/doc-entry'
// Types
import type { ConsultationFormData } from '~/schemas/consultation.schema.ts'
// Helpers
import type z from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import Textarea from '~/components/pub/ui/textarea/Textarea.vue'
interface Props {
schema: z.ZodSchema<any>
values: any
units: { value: string; label: string }[]
isLoading?: boolean
isReadonly?: boolean
}
const props = defineProps<Props>()
const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const emit = defineEmits<{
submit: [values: ConsultationFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
id: 0,
encounter_id: 0,
problem: '',
unit_id: 0,
} as Partial<ConsultationFormData>,
})
const [unit_id, unitAttrs] = defineField('unit_id')
const [problem, problemAttrs] = defineField('problem')
// Fill fields from props.values if provided
if (props.values) {
if (props.values.unit_id !== undefined) unit_id.value = props.values.unit_id
if (props.values.code !== undefined) problem.value = props.values.problem
}
const resetForm = () => {
unit_id.value = 0
problem.value = ''
}
// Form submission handler
function onSubmitForm(values: any) {
const formData: ConsultationFormData = {
id: 0,
encounter_id: 0,
problem: problem.value || '',
unit_id: unit_id.value || 0,
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onCancelForm() {
emit('cancel', resetForm)
}
</script>
<template>
<form id="form-division" @submit.prevent>
<DE.Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="3" :cellFlex="false">
<DE.Cell>
<DE.Label>Dokter Asal</DE.Label>
<DE.Field :errMessage="errors.code">
<Input readonly />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Tanggal</DE.Label>
<DE.Field :errMessage="errors.code">
<Input readonly />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Unit</DE.Label>
<DE.Field :errMessage="errors.unit_id">
{{ errors.unit_id }}
<Select
id="strUnit_id"
v-model.number="unit_id"
icon-name="i-lucide-chevron-down"
placeholder="Pilih kelompok obat"
v-bind="unitAttrs"
:items="props.units || []"
:disabled="isLoading || isReadonly"
/>
<!-- <Input type="number" id="unit_id" v-model.number="unit_id" v-bind="unitAttrs" :disabled="isLoading || isReadonly" /> -->
</DE.Field>
</DE.Cell>
<DE.Cell :colSpan="3">
<DE.Label>Uraian</DE.Label>
<DE.Field :errMessage="errors.problem">
<Textarea id="problem" v-model="problem" v-bind="problemAttrs" :disabled="isLoading || isReadonly" />
</DE.Field>
</DE.Cell>
</DE.Block>
<div class="my-2 flex justify-end gap-2 py-2">
<Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button>
<Button
v-if="!isReadonly"
type="button"
class="w-[120px]"
:disabled="isLoading || !meta.valid"
@click="onSubmitForm"
>
Simpan
</Button>
</div>
</form>
</template>
+40
View File
@@ -0,0 +1,40 @@
import type {
Col,
KeyLabel,
RecComponent,
RecStrFuncComponent,
RecStrFuncUnknown,
Th,
} from '~/components/pub/custom-ui/data/types'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-ud.vue'))
export const cols: Col[] = [{ width: 100 }, {}, {}, {}, { width: 50 }]
export const header: Th[][] = [[{ label: 'Tanggal' }, { label: 'Dokter' }, { label: 'Tujuan' }, { label: 'Pertanyaan' }, { label: 'Jawaban' }, { label: '' }]]
export const keys = ['date', 'dstDoctor.name', 'dstUnit.name', 'case', 'solution', 'action']
export const delKeyNames: KeyLabel[] = [
{ key: 'data', label: 'Tanggal' },
{ key: 'dstDoctor.name', label: 'Dokter' },
]
export const funcComponent: RecStrFuncComponent = {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
props: {
size: 'sm',
},
}
return res
},
}
export const funcHtml: RecStrFuncUnknown = {}
+36
View File
@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/custom-ui/pagination/pagination.type'
import PaginationView from '~/components/pub/custom-ui/pagination/pagination-view.vue'
import { cols, funcComponent, funcHtml, header, keys } from './list'
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">
<PubBaseDataTable
:rows="data"
:cols="cols"
:header="header"
:keys="keys"
:func-html="funcHtml"
:func-component="funcComponent"
:skeleton-size="paginationMeta?.pageSize"
/>
<!-- FIXME: pindahkan ke content/division/list.vue -->
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -0,0 +1,179 @@
<script setup lang="ts">
// Components
import Dialog from '~/components/pub/base/modal/dialog.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import List from '~/components/app/consultation/list.vue'
import Entry from '~/components/app/consultation/entry.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/custom-ui/data/types'
import { ConsultationSchema, type ConsultationFormData } from '~/schemas/consultation.schema'
import type { Unit } from '~/models/unit'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/consultation.handler'
// Services
import { getList, getDetail } from '~/services/consultation.service'
// import { getList as getUnitList } from '~/services/unit.service' // previously uses getList
import { getValueLabelList } from '~/services/unit.service'
let units = ref<{ value: string; label: string }[]>([])
const title = ref('')
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getMyList,
} = usePaginatedList({
fetchFn: async ({ page, search }) => {
const result = await getList({ search, page })
return { success: result.success || false, body: result.body || {} }
},
entityName: 'consultation',
})
const headerPrep: HeaderPrep = {
title: 'Konsultasi',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (value: string) => {
searchInput.value = value
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getMyDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
// const getUnits = async () => {
// const result = await getUnitList()
// if (result.success) {
// const currentMedicineGroups = result.body?.data || []
// units.value = currentMedicineGroups.map((item: Unit) => ({
// value: item.code,
// label: item.name,
// }))
// }
// }
// Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getMyDetail(recId.value)
title.value = 'Detail Konsultasi'
isReadonly.value = true
break
case ActionEvents.showEdit:
getMyDetail(recId.value)
title.value = 'Edit Konsultasi'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
onMounted(async () => {
await getMyList()
units.value = await getValueLabelList()
})
</script>
<template>
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<List :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" :title="!!recItem ? title : 'Tambah Divisi'" size="xl" prevent-outside>
<Entry
:schema="ConsultationSchema"
:values="recItem"
:units="units"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: ConsultationFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getMyList, resetForm, toast)
return
}
handleActionSave(values, getMyList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/>
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getMyList, toast)"
@cancel=""
>
<template #default="{ record }">
<div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p>
</div>
</template>
</RecordConfirmation>
</template>
+17
View File
@@ -0,0 +1,17 @@
import { genCrudHandler } from '~/handlers/_handler'
import { create, update, remove } from '~/services/consultation.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({ create, update, remove })
+45
View File
@@ -0,0 +1,45 @@
export interface Consultation {
id: number
encounter_id: number
unit_id: number
doctor_id?: number
problem: string
solution?: string
repliedAt?: string
}
export interface CreateDto {
encounter_id: number
problem: string
unit_id: number
}
export interface UpdateDto {
id: number
problem: string
unit_id: number
}
export interface DeleteDto {
id: number
}
export function genCreateDto(): CreateDto {
return {
encounter_id: 0,
problem: '',
unit_id: 0,
}
}
export function genConsultation(): Consultation {
return {
id: 0,
encounter_id: 0,
unit_id: 0,
doctor_id: 0,
problem: '',
solution: '',
repliedAt: '',
}
}
+42
View File
@@ -0,0 +1,42 @@
<script setup lang="ts">
// import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/base/error/error.vue'
import List from '~/components/content/consultation/list.vue'
// import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
// middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Daftar Divisi',
contentFrame: 'cf-container-lg',
})
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)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
// const canRead = hasReadAccess(roleAccess)
const canRead = true
</script>
<template>
<div>
<div v-if="canRead">
<List />
</div>
<Error v-else :status-code="403" />
</div>
</template>
+12
View File
@@ -0,0 +1,12 @@
import { z } from 'zod'
import type { Consultation } from '~/models/consultation'
const ConsultationSchema = z.object({
unit_id: z.number({ required_error: 'Unit harus diisi' }),
problem: z.string({ required_error: 'Uraian harus diisi' }).min(20, 'Kode minimum 20 karakter'),
})
type ConsultationFormData = z.infer<typeof ConsultationSchema> & (Consultation)
export { ConsultationSchema }
export type { ConsultationFormData }
+23
View File
@@ -0,0 +1,23 @@
import * as base from './_crud-base'
const path = '/api/v1/consultation'
export function create(data: any) {
return base.create(path, data)
}
export function getList(params: any = null) {
return base.getList(path, params)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data)
}
export function remove(id: number | string) {
return base.remove(path, id)
}
+38
View File
@@ -1,5 +1,43 @@
import { xfetch } from '~/composables/useXfetch'
import * as base from './_crud-base'
import type { Unit } from '~/models/unit'
const path = '/api/v1/unit'
export function create(data: any) {
return base.create(path, data)
}
export function getList(params: any = null) {
return base.getList(path, params)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data)
}
export function remove(id: number | string) {
return base.remove(path, id)
}
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
let data: { value: string; label: string }[] = [];
const result = await getList(params)
if (result.success) {
const resultData = result.body?.data || []
data = resultData.map((item: Unit) => ({
value: item.id,
label: item.name,
}))
}
return data;
}
//////////////////////// WILL BE LEGACY
const mainUrl = '/api/v1/unit'
export async function getUnits(params: any = null) {