Merge branch 'dev' of https://github.com/dikstub-rssa/simrs-fe into feat/integrasi-assessment-medis-114

This commit is contained in:
Abizrh
2025-10-22 17:21:09 +07:00
61 changed files with 3955 additions and 9 deletions
+126
View File
@@ -0,0 +1,126 @@
<script setup lang="ts">
// Components
import * as DE from '~/components/pub/my-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'
import type { CreateDto } from '~/models/consultation'
interface Props {
schema: z.ZodSchema<any>
values: CreateDto
encounter_id: number
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 today = new Date()
const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
date: props.values.date || today.toISOString().slice(0, 10),
problem: '',
dstUnit_id: 0,
} as Partial<ConsultationFormData>,
})
const [date, dateAttrs] = defineField('date')
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.date !== undefined) date.value = props.values.date.substring(0, 10)
if (props.values.dstUnit_id !== undefined) unit_id.value = props.values.dstUnit_id
if (props.values.problem !== undefined) problem.value = props.values.problem
}
const resetForm = () => {
date.value = date.value ?? today.toISOString().slice(0, 10)
unit_id.value = 0
problem.value = ''
}
// Form submission handler
function onSubmitForm(values: any) {
const formData: ConsultationFormData = {
encounter_id: props.encounter_id,
date: date.value ? `${date.value}T00:00:00Z` : '',
problem: problem.value || '',
dstUnit_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>Tanggal</DE.Label>
<DE.Field :errMessage="errors.code">
<Input
v-model.number="date"
icon-name="i-lucide-chevron-down"
v-bind="dateAttrs"
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 poliklinik tujuan"
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>
@@ -0,0 +1,54 @@
import type { Config, RecComponent, RecStrFuncComponent, RecStrFuncUnknown } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
import type { Consultation } from '~/models/consultation'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
export const config: Config = {
cols: [{ width: 100 }, {}, {}, {}, { width: 50 }],
headers: [[
{ label: 'Tanggal' },
{ label: 'Tujuan' },
{ label: 'Dokter' },
{ label: 'Pertanyaan' },
{ label: 'Jawaban' },
{ label: '' },
]],
keys: ['date', 'dstUnit.name', 'dstDoctor.name', 'problem', 'solution', 'action'],
delKeyNames: [
{ key: 'data', label: 'Tanggal' },
{ key: 'dstDoctor.name', label: 'Dokter' },
],
parses: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
props: {
size: 'sm',
},
}
return res
},
date(rec) {
const recX = rec as Consultation
return recX.date?.substring(0, 10) || '-'
}
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
props: {
size: 'sm',
},
}
return res
},
} as RecStrFuncComponent,
htmls: {} as RecStrFuncUnknown,
}
+31
View File
@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
import { config } from './list.cfg'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
const props = 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"
/>
<!-- FIXME: pindahkan ke content/division/list.vue -->
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -0,0 +1,36 @@
import { defineAsyncComponent } from 'vue'
import type { Config } from '~/components/pub/my-ui/data-table'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
export const config: Config = {
cols: [{}, {}, { width: 50 }],
headers: [[{ label: 'Nama' }, { label: 'Jumlah' }, { label: '' }]],
keys: ['name', 'count', 'action'],
delKeyNames: [
{ key: 'name', label: 'Nama' },
{ key: 'count', label: 'Jumlah' },
],
skeletonSize: 10
// funcParsed: {
// parent: (rec: unknown): unknown => {
// const recX = rec as SmallDetailDto
// return recX.parent?.name || '-'
// },
// },
// funcComponent: {
// action(rec: object, idx: any) {
// const res: RecComponent = {
// idx,
// rec: rec as object,
// component: action,
// props: {
// size: 'sm',
// },
// }
// return res
// },
// }
}
@@ -0,0 +1,13 @@
<script setup lang="ts">
import DataTable from '~/components/pub/my-ui/data-table/data-table.vue'
import { config } from './list-entry.config'
</script>
<template>
<DataTable v-bind="config" :rows="[]" class="border mb-3 2xl:mb-4" />
<div>
<Button>
Tambah
</Button>
</div>
</template>
@@ -0,0 +1,6 @@
<script setup lang="ts">
</script>
<template>
Test
</template>
@@ -0,0 +1,42 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import type { DeviceOrder } from '~/models/device-order'
import { defineAsyncComponent } from 'vue'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
export const config: Config = {
cols: [{ width: 120 }, { }, { }, { width: 50 }],
headers: [[{ label: 'Tanggal' }, { label: 'DPJP' }, { label: 'Alat Kesehatan' }, { label: '' }]],
keys: ['createdAt', 'encounter.doctor.person.name', 'items', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
skeletonSize: 10,
htmls: {
items: (rec: unknown): unknown => {
const recX = rec as DeviceOrder
return recX.items?.length || 0
},
}
// funcParsed: {
// parent: (rec: unknown): unknown => {
// const recX = rec as SmallDetailDto
// return recX.parent?.name || '-'
// },
// },
// funcComponent: {
// action(rec: object, idx: any) {
// const res: RecComponent = {
// idx,
// rec: rec as object,
// component: action,
// props: {
// size: 'sm',
// },
// }
// return res
// },
// }
}
+35
View File
@@ -0,0 +1,35 @@
<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.config'
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="[]"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -0,0 +1,145 @@
<script setup lang="ts">
import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import TreeSelect from '~/components/pub/my-ui/select-tree/tree-select.vue'
import Combobox from '~/components/pub/my-ui/form/combobox.vue'
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 { Form } from '~/components/pub/ui/form'
interface DivisionFormData {
name: string
code: string
parentId: string
}
const props = defineProps<{
division: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
}[]
}
divisionTree?: {
msg: {
placeholder: string
search: string
empty: string
}
data: TreeItem[]
onFetchChildren: (parentId: string) => Promise<void>
}
schema: any
initialValues?: Partial<DivisionFormData>
errors?: FormErrors
}>()
const emit = defineEmits<{
'submit': [values: DivisionFormData, resetForm: () => void]
'cancel': [resetForm: () => void]
}>()
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: DivisionFormData = {
name: values.name || '',
code: values.code || '',
parentId: values.parentId || '',
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) {
emit('cancel', resetForm)
}
</script>
<template>
<Form
v-slot="{ handleSubmit, resetForm }" as="" keep-values :validation-schema="formSchema"
:initial-values="initialValues"
>
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<FieldGroup>
<Label label-for="name">Nama</Label>
<Field id="name" :errors="errors">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormControl>
<Input
id="name" type="text" placeholder="Masukkan nama divisi" autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="code">Kode</Label>
<Field id="code" :errors="errors">
<FormField v-slot="{ componentField }" name="code">
<FormItem>
<FormControl>
<Input id="code" type="text" placeholder="Masukkan kode divisi" autocomplete="off" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="parentId">Divisi Induk</Label>
<Field id="parentId" :errors="errors">
<FormField v-slot="{ componentField }" name="parentId">
<FormItem>
<FormControl>
<!-- Gunakan TreeSelect jika divisionTree tersedia, fallback ke Combobox -->
<TreeSelect
v-if="props.divisionTree"
id="parentId"
:model-value="componentField.modelValue"
:data="props.divisionTree.data"
:on-fetch-children="props.divisionTree.onFetchChildren"
@update:model-value="componentField.onChange"
/>
<Combobox
v-else
id="parentId" v-bind="componentField" :items="props.division.items"
:placeholder="props.division.msg.placeholder" :search-placeholder="props.division.msg.search"
:empty-message="props.division.msg.empty"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<Button type="button" variant="outline" @click="onCancelForm({ resetForm })">
Batal
</Button>
<Button type="submit">
Simpan
</Button>
</div>
</form>
</Form>
</template>
+165
View File
@@ -0,0 +1,165 @@
<script setup lang="ts">
import TreeSelect from '~/components/pub/my-ui/select-tree/tree-select.vue'
/**
* DEMO COMPONENT - Tree Select dengan Lazy Loading
*
* Komponen ini adalah contoh penggunaan TreeSelect dengan data teknologi.
* Untuk penggunaan dalam aplikasi nyata, lihat komponen content/division/entry.vue
* yang menggunakan tree select untuk data divisi rumah sakit.
*/
// Tipe data untuk konsistensi
interface TreeItem {
value: string
label: string
hasChildren: boolean
children?: TreeItem[]
}
// State untuk data pohon demo - data teknologi sebagai contoh
const treeData = ref<TreeItem[]>([
{ value: 'frontend', label: 'Frontend Development', hasChildren: true },
{ value: 'backend', label: 'Backend Development', hasChildren: true },
{ value: 'mobile', label: 'Mobile Development', hasChildren: true },
{ value: 'devops', label: 'DevOps & Infrastructure', hasChildren: false },
])
// State untuk menampung nilai yang dipilih
const selectedValue = ref<string>()
// --- DEMO LOGIC: SIMULASI API DAN MANIPULASI DATA ---
// Helper: Fungsi rekursif untuk mencari dan menyisipkan data anak ke dalam state
function findAndInsertChildren(nodes: TreeItem[], parentId: string, newChildren: TreeItem[]): boolean {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node && node.value === parentId) {
// Gunakan Vue.set equivalent untuk memastikan reactivity
node.children = [...newChildren]
console.log(`[findAndInsertChildren] Updated children for ${parentId}:`, node.children)
return true
}
if (node && node.children && findAndInsertChildren(node.children, parentId, newChildren)) {
return true
}
}
return false
}
// Fungsi demo untuk simulasi fetch data dari API
async function handleFetchChildren(parentId: string): Promise<void> {
console.log(`[DEMO] Mengambil data anak untuk parent: ${parentId}`)
// Simulasi delay API call
await new Promise(resolve => setTimeout(resolve, 600))
let childrenData: TreeItem[] = []
// Sample data berdasarkan parent ID
switch (parentId) {
case 'frontend':
childrenData = [
{ value: 'vue', label: 'Vue.js', hasChildren: true },
{ value: 'react', label: 'React.js', hasChildren: true },
{ value: 'angular', label: 'Angular', hasChildren: false },
{ value: 'svelte', label: 'Svelte', hasChildren: false },
]
break
case 'backend':
childrenData = [
{ value: 'nodejs', label: 'Node.js', hasChildren: true },
{ value: 'python', label: 'Python', hasChildren: true },
{ value: 'golang', label: 'Go', hasChildren: false },
{ value: 'rust', label: 'Rust', hasChildren: false },
]
break
case 'mobile':
childrenData = [
{ value: 'flutter', label: 'Flutter', hasChildren: false },
{ value: 'react-native', label: 'React Native', hasChildren: false },
{ value: 'ionic', label: 'Ionic', hasChildren: false },
]
break
case 'vue':
childrenData = [
{ value: 'nuxt', label: 'Nuxt.js', hasChildren: false },
{ value: 'quasar', label: 'Quasar', hasChildren: false },
]
break
case 'react':
childrenData = [
{ value: 'nextjs', label: 'Next.js', hasChildren: false },
{ value: 'gatsby', label: 'Gatsby', hasChildren: false },
]
break
case 'nodejs':
childrenData = [
{ value: 'express', label: 'Express.js', hasChildren: false },
{ value: 'nestjs', label: 'NestJS', hasChildren: false },
{ value: 'fastify', label: 'Fastify', hasChildren: false },
]
break
case 'python':
childrenData = [
{ value: 'django', label: 'Django', hasChildren: false },
{ value: 'fastapi', label: 'FastAPI', hasChildren: false },
{ value: 'flask', label: 'Flask', hasChildren: false },
]
break
}
// Insert data ke dalam tree state
const success = findAndInsertChildren(treeData.value, parentId, childrenData)
console.log(`[DEMO] Insert children result:`, success)
// Force trigger reactivity
triggerRef(treeData)
console.log(`[DEMO] Current tree data:`, JSON.stringify(treeData.value, null, 2))
}
</script>
<template>
<div class="p-10 max-w-2xl mx-auto">
<div class="mb-6">
<h1 class="mb-2 text-3xl font-bold text-slate-800">Demo: Tree Select dengan Lazy Loading</h1>
<p class="text-slate-600">
Contoh penggunaan komponen TreeSelect dengan data teknologi.
Pilih item untuk melihat sub-kategori yang dimuat secara lazy.
</p>
</div>
<div class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h3 class="font-semibold text-blue-800 mb-2">💡 Catatan untuk Developer:</h3>
<p class="text-sm text-blue-700">
Untuk implementasi nyata dengan data divisi rumah sakit,
lihat komponen <code class="px-1 bg-blue-100 rounded">content/division/entry.vue</code>
</p>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-2">
Pilih Teknologi:
</label>
<TreeSelect
v-model="selectedValue"
:data="treeData"
:on-fetch-children="handleFetchChildren"
/>
</div>
<div class="p-4 bg-slate-50 rounded-lg">
<p class="text-sm text-slate-600 mb-1">Value yang terpilih:</p>
<span class="px-3 py-1 font-mono text-sm bg-white border rounded-md">
{{ selectedValue || 'Belum ada yang dipilih' }}
</span>
</div>
</div>
<div class="mt-8 text-xs text-slate-500">
<p>🔄 Data dimuat secara lazy saat node parent dibuka</p>
<p> Simulasi delay 600ms untuk menampilkan loading state</p>
</div>
</div>
</template>
@@ -0,0 +1,47 @@
<script setup lang="ts">
import Block from '~/components/pub/form/block.vue'
import FieldGroup from '~/components/pub/form/field-group.vue'
import Field from '~/components/pub/form/field.vue'
import Label from '~/components/pub/form/label.vue'
</script>
<template>
<form id="entry-form">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<Icon name="i-lucide-user" class="me-2" />
<span class="font-semibold">Tambah</span> Pasien
</div>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<Block>
<FieldGroup :column="3">
<Label>Nama</Label>
<Field>
<Input type="text" name="name" />
</Field>
</FieldGroup>
<FieldGroup :column="3">
<Label>Nama</Label>
<Field>
<Input type="text" name="name" />
</Field>
</FieldGroup>
<FieldGroup :column="3">
<Label>Nomor RM</Label>
<Field>
<Input type="text" name="name" />
</Field>
</FieldGroup>
<FieldGroup>
<Label dynamic>Alamat</Label>
<Field>
<Input type="text" name="name" />
</Field>
</FieldGroup>
</Block>
</div>
<div class="my-2 flex justify-end py-2">
<PubNavFooterCsd />
</div>
</form>
</template>
@@ -0,0 +1,112 @@
import type { Col, KeyLabel, RecComponent, RecStrFuncComponent, RecStrFuncUnknown, Th } from '~/components/pub/my-ui/data/types'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const cols: Col[] = [
{},
{},
{},
{ width: 100 },
{ width: 120 },
{},
{},
{},
{ width: 100 },
{ width: 100 },
{},
{ width: 50 },
]
export const header: Th[][] = [
[
{ label: 'Nama' },
{ label: 'Rekam Medis' },
{ label: 'KTP' },
{ label: 'Tgl Lahir' },
{ label: 'Umur' },
{ label: 'JK' },
{ label: 'Pendidikan' },
{ label: 'Status' },
{ label: '' },
],
]
export const keys = [
'name',
'medicalRecord_number',
'identity_number',
'birth_date',
'patient_age',
'gender',
'education',
'status',
'action',
]
export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
]
export const funcParsed: RecStrFuncUnknown = {
name: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return `${recX.firstName} ${recX.middleName || ''} ${recX.lastName || ''}`
},
identity_number: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (recX.identity_number?.substring(0, 5) === 'BLANK') {
return '(TANPA NIK)'
}
return recX.identity_number
},
birth_date: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (typeof recX.birth_date == 'object' && recX.birth_date) {
return (recX.birth_date as Date).toLocaleDateString()
} else if (typeof recX.birth_date == 'string') {
return (recX.birth_date as string).substring(0, 10)
}
return recX.birth_date
},
patient_age: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.birth_date?.split('T')[0]
},
gender: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (typeof recX?.gender_code !== 'number' && recX?.gender_code !== '') {
return 'Tidak Diketahui'
}
return recX.gender_code
},
education: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (typeof recX.education_code == 'number' && recX.education_code >= 0) {
return recX.education_code
} else if (typeof recX.education_code) {
return recX.education_code
}
return '-'
},
}
export const funcComponent: RecStrFuncComponent = {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
}
export const funcHtml: RecStrFuncUnknown = {
patient_address(_rec) {
return '-'
},
}
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
defineProps<{
data: any[]
}>()
</script>
<template>
<PubMyUiDataTable
:rows="data"
:cols="cols"
:header="header"
:keys="keys"
:func-parsed="funcParsed"
:func-html="funcHtml"
:func-component="funcComponent"
/>
</template>
+73
View File
@@ -0,0 +1,73 @@
<script setup lang="ts">
import AssesmentFunctionList from './assesment-function/list.vue'
const props = defineProps<{
initialActiveTab: string
}>()
const activeTab = ref(props.initialActiveTab)
const emit = defineEmits<{
changeTab: [value: string]
}>()
interface TabItem {
value: string
label: string
component?: any
props?: Record<string, any>
}
const tabs: TabItem[] = [
{ value: 'status', label: 'Status Masuk/Keluar' },
{ value: 'early-medical-assessment', label: 'Pengkajian Awal Medis' },
{ value: 'rehab-medical-assessment', label: 'Pengkajian Awal Medis Rehabilitasi Medis' },
{ value: 'function-assessment', label: 'Asesmen Fungsi', component: AssesmentFunctionList },
{ value: 'therapy-protocol', label: 'Protokol Terapi' },
{ value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' },
{ value: 'consent', label: 'General Consent' },
{ value: 'patient-note', label: 'CPRJ' },
{ value: 'prescription', label: 'Order Obat' },
{ value: 'device', label: 'Order Alkes' },
{ value: 'mcu-radiology', label: 'Order Radiologi' },
{ value: 'mcu-lab-pc', label: 'Order Lab PK' },
{ value: 'mcu-lab-micro', label: 'Order Lab Mikro' },
{ value: 'mcu-lab-pa', label: 'Order Lab PA' },
{ value: 'medical-action', label: 'Order Ruang Tindakan' },
{ value: 'mcu-result', label: 'Hasil Penunjang' },
{ value: 'consultation', label: 'Konsultasi' },
{ value: 'resume', label: 'Resume' },
{ value: 'control', label: 'Surat Kontrol' },
{ value: 'screening', label: 'Skrinning MPP' },
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung' },
]
function changeTab(value: string) {
activeTab.value = value;
emit('changeTab', value);
}
</script>
<template>
<!-- Tabs -->
<div class="mt-4 flex flex-wrap gap-2 rounded-md border bg-white p-4 shadow-sm">
<Button
v-for="tab in tabs"
:key="tab.value"
:data-active="activeTab === tab.value"
class="rounded-full transition data-[active=false]:bg-gray-100 data-[active=true]:bg-primary data-[active=false]:text-gray-700 data-[active=true]:text-white"
@click="changeTab(tab.value)"
>
{{ tab.label }}
</Button>
</div>
<!-- Active Tab Content -->
<div class="mt-4 rounded-md border p-4">
<component
v-if="tabs.find((t) => t.value === activeTab)?.component"
:is="tabs.find((t) => t.value === activeTab)?.component"
:label="tabs.find((t) => t.value === activeTab)?.label"
v-bind="tabs.find((t) => t.value === activeTab)?.props || {}"
/>
</div>
</template>
@@ -0,0 +1,181 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import Combobox from '~/components/pub/my-ui/form/combobox.vue'
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'
interface SpecialistFormData {
name: string
code: string
installationId: string
unitId: string
}
const props = defineProps<{
schema: any
initialValues?: Partial<SpecialistFormData>
errors?: FormErrors
installation: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
unit: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
}>()
const emit = defineEmits<{
'submit': [values: SpecialistFormData, resetForm: () => void]
'cancel': [resetForm: () => void]
'installationChanged': [id: string]
}>()
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: SpecialistFormData = {
name: values.name || '',
code: values.code || '',
installationId: values.installationId || '',
unitId: values.unitId || '',
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) {
emit('cancel', resetForm)
}
// Watch for installation changes
function onInstallationChanged(installationId: string, setFieldValue: any) {
setFieldValue('unitId', '', false)
emit('installationChanged', installationId || '')
}
</script>
<template>
<Form
v-slot="{ handleSubmit, resetForm, values, setFieldValue }" as="" keep-values :validation-schema="formSchema"
:initial-values="initialValues"
>
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<FieldGroup>
<Label label-for="name">Nama</Label>
<Field id="name" :errors="errors">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormControl>
<Input
id="name"
type="text"
placeholder="Masukkan nama spesialisasi"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="code">Kode</Label>
<Field id="code" :errors="errors">
<FormField v-slot="{ componentField }" name="code">
<FormItem>
<FormControl>
<Input
id="code"
type="text"
placeholder="Masukkan kode spesialisasi"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="installationId">Instalasi</Label>
<Field id="installationId" :errors="errors">
<FormField v-slot="{ componentField }" name="installationId">
<FormItem>
<FormControl>
<Combobox
id="installationId"
v-bind="componentField"
:items="installation.items"
:placeholder="installation.msg.placeholder"
:search-placeholder="installation.msg.search"
:empty-message="installation.msg.empty"
@update:model-value="(value) => onInstallationChanged(value, setFieldValue)"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="unitId">Unit</Label>
<Field id="unitId" :errors="errors">
<FormField v-slot="{ componentField }" name="unitId">
<FormItem>
<FormControl>
<Combobox
id="unitId"
:disabled="!values.installationId"
v-bind="componentField"
:items="unit.items"
:placeholder="unit.msg.placeholder"
:search-placeholder="unit.msg.search"
:empty-message="unit.msg.empty"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<Button type="button" variant="outline" @click="onCancelForm({ resetForm })">
Batal
</Button>
<Button type="submit">
Simpan
</Button>
</div>
</form>
</Form>
</template>
@@ -0,0 +1,213 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import Combobox from '~/components/pub/my-ui/form/combobox.vue'
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'
interface SubSpecialistFormData {
name: string
code: string
installationId: string
unitId: string
specialistId: string
}
const props = defineProps<{
schema: any
initialValues?: Partial<SubSpecialistFormData>
errors?: FormErrors
installation: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
unit: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
specialist: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
}>()
const emit = defineEmits<{
'submit': [values: SubSpecialistFormData, resetForm: () => void]
'cancel': [resetForm: () => void]
'installationChanged': [id: string]
'unitChanged': [id: string]
}>()
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: SubSpecialistFormData = {
name: values.name || '',
code: values.code || '',
installationId: values.installationId || '',
unitId: values.unitId || '',
specialistId: values.specialistId || '',
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) {
emit('cancel', resetForm)
}
// Watch for installation changes
function onInstallationChanged(installationId: string, setFieldValue: any) {
setFieldValue('unitId', '', false)
setFieldValue('specialistId', '', false)
emit('installationChanged', installationId || '')
}
function onUnitChanged(unitId: string, setFieldValue: any) {
setFieldValue('specialistId', '', false)
emit('unitChanged', unitId || '')
}
</script>
<template>
<Form
v-slot="{
handleSubmit,
resetForm,
values,
setFieldValue,
}" as="" keep-values :validation-schema="formSchema"
:initial-values="initialValues"
>
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<FieldGroup>
<Label label-for="name">Nama</Label>
<Field id="name" :errors="errors">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormControl>
<Input
id="name" type="text" placeholder="Masukkan nama spesialisasi" autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="code">Kode</Label>
<Field id="code" :errors="errors">
<FormField v-slot="{ componentField }" name="code">
<FormItem>
<FormControl>
<Input
id="code" type="text" placeholder="Masukkan kode spesialisasi" autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="installationId">Instalasi</Label>
<Field id="installationId" :errors="errors">
<FormField v-slot="{ componentField }" name="installationId">
<FormItem>
<FormControl>
<Combobox
id="installationId" v-bind="componentField" :items="installation.items"
:placeholder="installation.msg.placeholder" :search-placeholder="installation.msg.search"
:empty-message="installation.msg.empty"
@update:model-value="(value) => onInstallationChanged(value, setFieldValue)"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="unitId">Unit</Label>
<Field id="unitId" :errors="errors">
<FormField v-slot="{ componentField }" name="unitId">
<FormItem>
<FormControl>
<Combobox
id="unitId" :disabled="!values.installationId" v-bind="componentField" :items="unit.items"
:placeholder="unit.msg.placeholder" :search-placeholder="unit.msg.search"
:empty-message="unit.msg.empty"
@update:model-value="(value) => onUnitChanged(value, setFieldValue)"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="specialistId">Specialist</Label>
<Field id="specialistId" :errors="errors">
<FormField v-slot="{ componentField }" name="specialistId">
<FormItem>
<FormControl>
<Combobox
id="specialistId" :disabled="!values.unitId" v-bind="componentField" :items="specialist.items"
:placeholder="specialist.msg.placeholder" :search-placeholder="specialist.msg.search"
:empty-message="specialist.msg.empty"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<Button type="button" variant="outline" @click="onCancelForm({ resetForm })">
Batal
</Button>
<Button type="submit">
Simpan
</Button>
</div>
</form>
</Form>
</template>
+125
View File
@@ -0,0 +1,125 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import Combobox from '~/components/pub/my-ui/form/combobox.vue'
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'
interface UnitFormData {
name: string
code: string
parentId: string
}
const props = defineProps<{
unit: {
msg: {
placeholder: string
search: string
empty: string
}
items: {
value: string
label: string
code: string
}[]
}
schema: any
initialValues?: Partial<UnitFormData>
errors?: FormErrors
}>()
const emit = defineEmits<{
'submit': [values: UnitFormData, resetForm: () => void]
'cancel': [resetForm: () => void]
}>()
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: UnitFormData = {
name: values.name || '',
code: values.code || '',
parentId: values.parentId || '',
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) {
emit('cancel', resetForm)
}
</script>
<template>
<Form
v-slot="{ handleSubmit, resetForm }" as="" keep-values :validation-schema="formSchema"
:initial-values="initialValues"
>
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<FieldGroup>
<Label label-for="name">Nama</Label>
<Field id="name" :errors="errors">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormControl>
<Input
id="name" type="text" placeholder="Masukkan nama unit" autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="code">Kode</Label>
<Field id="code" :errors="errors">
<FormField v-slot="{ componentField }" name="code">
<FormItem>
<FormControl>
<Input id="code" type="text" placeholder="Masukkan kode unit" autocomplete="off" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="parentId">Instalasi</Label>
<Field id="parentId" :errors="errors">
<FormField v-slot="{ componentField }" name="parentId">
<FormItem>
<FormControl>
<Combobox
id="parentId" v-bind="componentField" :items="unit.items"
:placeholder="unit.msg.placeholder" :search-placeholder="unit.msg.search"
:empty-message="unit.msg.empty"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<Button type="button" variant="outline" @click="onCancelForm({ resetForm })">
Batal
</Button>
<Button type="submit">
Simpan
</Button>
</div>
</form>
</Form>
</template>
@@ -0,0 +1,213 @@
<script setup lang="ts">
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/my-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/my-ui/data/types'
import { ConsultationSchema, type ConsultationFormData } from '~/schemas/consultation.schema'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/consultation.handler'
// Services
import { getList, getDetail } from '~/services/consultation.service'
import { getValueLabelList } from '~/services/unit.service'
// Models
import type { Encounter } from '~/models/encounter'
// Props
interface Props {
encounter: Encounter
}
const props = defineProps<Props>()
let units = ref<{ value: string; label: string }[]>([])
const encounterId = ref<number>(props?.encounter?.id || 0)
const title = ref('')
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getMyList,
} = usePaginatedList({
fetchFn: async ({ page, search }) => {
const result = await getList({ 'encounter-id': props.encounter.id, includes: 'encounter,dstUnit', search, page })
if (result.success) {
data.value = result.body.data
}
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 = {
encounter_id: encounterId.value,
date: today.toISOString().slice(0, 10),
unit_id: 0,
problem: '',
}
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
const today = new Date()
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
}
}
// 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?.id ? 'Edit Konsultasi' : 'Tambah Konsultasi'"
size="xl"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<Entry
:schema="ConsultationSchema"
:values="recItem"
:encounter_id="encounterId"
: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>
@@ -0,0 +1,37 @@
<script setup lang="ts">
import Nav from '~/components/pub/my-ui/nav-footer/ba-su.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
import ItemListEntry from '~/components/app/device-order-item/list-entry.vue'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
const { backToList } = useQueryCRUDMode()
const headerPrep: HeaderPrep = {
title: 'Tambah Order Alkes',
icon: 'i-lucide-box',
}
function navClick(type: 'cancel' | 'submit') {
if (type === 'cancel') {
backToList()
}
}
</script>
<template>
<Header
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5"
/>
<ItemListEntry />
<Separator class="my-5" />
<div class="w-full flex justify-center">
<Nav @click="navClick" />
</div>
</template>
@@ -0,0 +1,145 @@
<script setup lang="ts">
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import List from '~/components/app/device-order/list.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
// import { useQueryMode } from '~/composables/useQueryMode'
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { type DeviceOrderFormData, DeviceOrderSchema } from '~/schemas/device-order.schema'
import type { DeviceOrder } from "~/models/device-order";
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/device-order.handler'
// Services
import { getList } from '~/services/device-order.service'
const route = useRoute()
const title = ref('')
// const { mode, openForm, backToList } = useQueryMode()
const { mode, goToEntry, backToList } = useQueryCRUDMode()
const { recordId } = useQueryCRUDRecordId()
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getMyList,
} = usePaginatedList({
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10,
includes: 'parent,childrens',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'device-order',
})
const headerPrep: HeaderPrep = {
title: 'Order Alkes',
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: async () => {
recItem.value = null
recId.value = 0
isReadonly.value = false
// await handleActionSave(recItem, getMyList, () => {}, () => {})
goToEntry()
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// Watch for row actions when recId or recAction changes
onMounted(async () => {
await getMyList()
})
</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"
/>
<!-- 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>
@@ -0,0 +1,12 @@
<script setup lang="ts">
//
import List from './list.vue'
import Entry from './entry.vue'
const { mode } = useQueryMode()
</script>
<template>
<List v-if="mode === 'list'" />
<Entry v-else />
</template>
+145
View File
@@ -0,0 +1,145 @@
import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
import * as z from 'zod'
export const divisionConf = {
msg: {
placeholder: '---pilih divisi utama',
search: 'kode, nama divisi',
empty: 'divisi tidak ditemukan',
},
items: [
{ value: '1', label: 'Medical' },
{ value: '2', label: 'Nursing' },
{ value: '3', label: 'Admin' },
{ value: '4', label: 'Support' },
{ value: '5', label: 'Education' },
{ value: '6', label: 'Pharmacy' },
{ value: '7', label: 'Radiology' },
{ value: '8', label: 'Laboratory' },
{ value: '9', label: 'Finance' },
{ value: '10', label: 'Human Resources' },
{ value: '11', label: 'IT Services' },
{ value: '12', label: 'Maintenance' },
{ value: '13', label: 'Catering' },
{ value: '14', label: 'Security' },
{ value: '15', label: 'Emergency' },
{ value: '16', label: 'Surgery' },
{ value: '17', label: 'Outpatient' },
{ value: '18', label: 'Inpatient' },
{ value: '19', label: 'Rehabilitation' },
{ value: '20', label: 'Research' },
],
}
export const schema = z.object({
name: z
.string({
required_error: 'Nama wajib diisi',
})
.min(1, 'Nama divisi wajib diisi'),
code: z
.string({
required_error: 'Kode wajib diisi',
})
.min(1, 'Kode divisi wajib diisi'),
parentId: z.string().optional(),
})
// State untuk tree data divisi - dimulai dengan data level atas
const divisionTreeData = ref<TreeItem[]>([
{ value: '1', label: 'Medical', hasChildren: true },
{ value: '2', label: 'Nursing', hasChildren: true },
{ value: '3', label: 'Admin', hasChildren: false },
{ value: '4', label: 'Support', hasChildren: true },
{ value: '5', label: 'Education', hasChildren: false },
{ value: '6', label: 'Pharmacy', hasChildren: true },
{ value: '7', label: 'Radiology', hasChildren: false },
{ value: '8', label: 'Laboratory', hasChildren: true },
])
// Helper function untuk mencari dan menyisipkan data anak ke dalam tree
function findAndInsertChildren(nodes: TreeItem[], parentId: string, newChildren: TreeItem[]): boolean {
for (const node of nodes) {
if (node.value === parentId) {
node.children = newChildren
return true
}
if (node.children && findAndInsertChildren(node.children as TreeItem[], parentId, newChildren)) {
return true
}
}
return false
}
// Fungsi untuk fetch data anak divisi (lazy loading)
async function handleFetchDivisionChildren(parentId: string): Promise<void> {
console.log(`Mengambil data sub-divisi untuk parent: ${parentId}`)
// Simulasi delay API call
await new Promise((resolve) => setTimeout(resolve, 800))
let childrenData: TreeItem[] = []
// Sample data berdasarkan parent ID
switch (parentId) {
case '1': // Medical
childrenData = [
{ value: '1-1', label: 'Cardiology', hasChildren: true },
{ value: '1-2', label: 'Neurology', hasChildren: false },
{ value: '1-3', label: 'Oncology', hasChildren: false },
]
break
case '2': // Nursing
childrenData = [
{ value: '2-1', label: 'ICU Nursing', hasChildren: false },
{ value: '2-2', label: 'ER Nursing', hasChildren: false },
{ value: '2-3', label: 'Ward Nursing', hasChildren: true },
]
break
case '4': // Support
childrenData = [
{ value: '4-1', label: 'IT Support', hasChildren: false },
{ value: '4-2', label: 'Maintenance', hasChildren: false },
]
break
case '6': // Pharmacy
childrenData = [
{ value: '6-1', label: 'Inpatient Pharmacy', hasChildren: false },
{ value: '6-2', label: 'Outpatient Pharmacy', hasChildren: false },
]
break
case '8': // Laboratory
childrenData = [
{ value: '8-1', label: 'Clinical Lab', hasChildren: false },
{ value: '8-2', label: 'Pathology Lab', hasChildren: false },
]
break
case '1-1': // Cardiology sub-divisions
childrenData = [
{ value: '1-1-1', label: 'Cardiac Surgery', hasChildren: false },
{ value: '1-1-2', label: 'Cardiac Cathlab', hasChildren: false },
]
break
case '2-3': // Ward Nursing sub-divisions
childrenData = [
{ value: '2-3-1', label: 'Pediatric Ward', hasChildren: false },
{ value: '2-3-2', label: 'Surgical Ward', hasChildren: false },
]
break
}
// Insert data ke dalam tree state
findAndInsertChildren(divisionTreeData.value, parentId, childrenData)
}
export const divisionTreeConfig = computed(() => ({
msg: {
placeholder: '--- Pilih divisi induk',
search: 'Cari divisi...',
empty: 'Divisi tidak ditemukan',
},
data: divisionTreeData.value,
onFetchChildren: handleFetchDivisionChildren,
}))
@@ -0,0 +1,208 @@
<script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
import AppDivisionEntryForm from '~/components/app/division/entry-form.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import Header from '~/components/pub/my-ui/nav-header/header.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { divisionConf, divisionTreeConfig, schema } from './entry'
// #region State & Computed
// Dialog state
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
// Table action rowId provider
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
async function fetchDivisionData(_params: any) {
const endpoint = '/api/v1/_dev/division/list'
return await xfetch(endpoint)
}
// Menggunakan composable untuk pagination
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getDivisionList,
} = usePaginatedList({
fetchFn: fetchDivisionData,
entityName: 'division',
})
const headerPrep: HeaderPrep = {
title: 'Divisi',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (_val: string) => {
// Handle search input - this will be triggered by the header component
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
},
},
addNav: {
label: 'Tambah Divisi',
icon: 'i-lucide-send',
onClick: () => {
isFormEntryDialogOpen.value = true
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// #endregion
// #region Functions
async function handleDeleteRow(record: any) {
try {
// TODO : hit backend request untuk delete
console.log('Deleting record:', record)
// Simulate API call
// const response = await xfetch(`/api/v1/division/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getDivisionList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
}
}
// #endregion region
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
setTimeout(() => {
resetForm()
}, 500)
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/division', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Refresh data after successful submission
await getDivisionList()
// TODO: Show success message
console.log('Division created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
}
}
// #endregion
// #region Watchers
// Watch for row actions
watch(recId, () => {
switch (recAction.value) {
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// Handle confirmation result
function handleConfirmDelete(record: any, action: string) {
console.log('Confirmed action:', action, 'for record:', record)
handleDeleteRow(record)
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion
</script>
<template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
<AppDivisonList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" title="Tambah Divisi" size="lg" prevent-outside>
<AppDivisionEntryFormPrev
:division="divisionConf" :division-tree="divisionTreeConfig" :schema="schema"
:initial-values="{ name: '', code: '', parentId: '' }" @submit="onSubmitForm" @cancel="onCancelForm"
/>
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation"
>
<template #default="{ record }">
<div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p>
</div>
</template>
</RecordConfirmation>
</template>
<style scoped>
/* component style */
</style>
@@ -0,0 +1,3 @@
<template>
<div>halo</div>
</template>
@@ -0,0 +1,64 @@
<script setup lang="ts">
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import AssesmentFunctionList from '~/components/app/encounter/assesment-function/list.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
const props = defineProps<{
label: string
}>()
const data = ref([])
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (_val: string) => {
// filter patient list
},
onClear: () => {
// clear url param
},
}
// Loading state management
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
})
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const hreaderPrep: HeaderPrep = {
title: props.label,
icon: 'i-lucide-users',
addNav: {
label: 'Tambah',
onClick: () => navigateTo('/rehab/registration-queue/sep-prosedur/add'),
},
}
async function getPatientList() {
const resp = await xfetch('/api/v1/patient')
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
}
}
onMounted(() => {
getPatientList()
})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
</script>
<template>
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
<AssesmentFunctionList :data="data" />
</div>
</template>
+2 -1
View File
@@ -14,6 +14,7 @@ import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue' import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
import PrescriptionList from '~/components/content/prescription/list.vue' import PrescriptionList from '~/components/content/prescription/list.vue'
import Status from '~/components/app/encounter/status.vue' import Status from '~/components/app/encounter/status.vue'
import Consultation from '~/components/content/consultation/list.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -55,7 +56,7 @@ const tabs: TabItem[] = [
{ value: 'mcu-lab-pa', label: 'Order Lab PA' }, { value: 'mcu-lab-pa', label: 'Order Lab PA' },
{ value: 'medical-action', label: 'Order Ruang Tindakan' }, { value: 'medical-action', label: 'Order Ruang Tindakan' },
{ value: 'mcu-result', label: 'Hasil Penunjang' }, { value: 'mcu-result', label: 'Hasil Penunjang' },
{ value: 'consultation', label: 'Konsultasi' }, { value: 'consultation', label: 'Konsultasi', component: Consultation, props: { encounter: data } },
{ value: 'resume', label: 'Resume' }, { value: 'resume', label: 'Resume' },
{ value: 'control', label: 'Surat Kontrol' }, { value: 'control', label: 'Surat Kontrol' },
{ value: 'screening', label: 'Skrinning MPP' }, { value: 'screening', label: 'Skrinning MPP' },
@@ -0,0 +1,37 @@
import * as z from 'zod'
export const installationConf = {
msg: {
placeholder: '---pilih encounter class (fhir7)',
},
items: [
{ value: '1', label: 'Ambulatory', code: 'AMB' },
{ value: '2', label: 'Inpatient', code: 'IMP' },
{ value: '3', label: 'Emergency', code: 'EMER' },
{ value: '4', label: 'Observation', code: 'OBSENC' },
{ value: '5', label: 'Pre-admission', code: 'PRENC' },
{ value: '6', label: 'Short Stay', code: 'SS' },
{ value: '7', label: 'Virtual', code: 'VR' },
{ value: '8', label: 'Home Health', code: 'HH' },
],
}
export const schemaConf = z.object({
name: z
.string({
required_error: 'Nama instalasi harus diisi',
})
.min(3, 'Nama instalasi minimal 3 karakter'),
code: z
.string({
required_error: 'Kode instalasi harus diisi',
})
.min(3, 'Kode instalasi minimal 3 karakter'),
encounterClassCode: z
.string({
required_error: 'Kelompok encounter class harus dipilih',
})
.min(1, 'Kelompok encounter class harus dipilih'),
})
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts"></script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<Icon name="i-lucide-user" class="me-2" />
<span class="font-semibold">Tambah</span> Pasien
</div>
<AppPatientEntryForm />
</template>
@@ -0,0 +1,95 @@
import * as z from 'zod'
export const schemaConf = z.object({
name: z
.string({
required_error: 'Nama spesialisasi harus diisi',
})
.min(3, 'Nama spesialisasi minimal 3 karakter'),
code: z
.string({
required_error: 'Kode spesialisasi harus diisi',
})
.min(3, 'Kode spesialisasi minimal 3 karakter'),
installationId: z
.string({
required_error: 'Instalasi harus dipilih',
})
.min(1, 'Instalasi harus dipilih'),
unitId: z
.string({
required_error: 'Unit harus dipilih',
})
.min(1, 'Unit harus dipilih'),
})
// Unit mapping berdasarkan installation
export const installationUnitMapping: Record<string, string[]> = {
'1': ['1', '3', '5'],
'2': ['2', '4', '6'],
'3': ['7', '8', '9', '10', '11'],
}
export const unitConf = {
msg: {
placeholder: '---pilih unit',
search: 'kode, nama unit',
empty: 'unit tidak ditemukan',
},
items: [
{ value: '1', label: 'Instalasi Medis', code: 'MED' },
{ value: '2', label: 'Instalasi Keperawatan', code: 'NUR' },
{ value: '3', label: 'Instalasi Administrasi', code: 'ADM' },
{ value: '4', label: 'Instalasi Penunjang Non-Medis', code: 'SUP' },
{ value: '5', label: 'Instalasi Pendidikan & Pelatihan', code: 'EDU' },
{ value: '6', label: 'Instalasi Farmasi', code: 'PHA' },
{ value: '7', label: 'Instalasi Radiologi', code: 'RAD' },
{ value: '8', label: 'Instalasi Laboratorium', code: 'LAB' },
{ value: '9', label: 'Instalasi Keuangan', code: 'FIN' },
{ value: '10', label: 'Instalasi SDM', code: 'HR' },
{ value: '11', label: 'Instalasi Teknologi Informasi', code: 'ITS' },
{ value: '12', label: 'Instalasi Pemeliharaan & Sarana', code: 'MNT' },
{ value: '13', label: 'Instalasi Gizi / Catering', code: 'CAT' },
{ value: '14', label: 'Instalasi Keamanan', code: 'SEC' },
{ value: '15', label: 'Instalasi Gawat Darurat', code: 'EMR' },
{ value: '16', label: 'Instalasi Bedah Sentral', code: 'SUR' },
{ value: '17', label: 'Instalasi Rawat Jalan', code: 'OUT' },
{ value: '18', label: 'Instalasi Rawat Inap', code: 'INP' },
{ value: '19', label: 'Instalasi Rehabilitasi Medik', code: 'REB' },
{ value: '20', label: 'Instalasi Penelitian & Pengembangan', code: 'RSH' },
],
}
export const installationConf = {
msg: {
placeholder: '---pilih instalasi',
search: 'kode, nama instalasi',
empty: 'instalasi tidak ditemukan',
},
items: [
{ value: '1', label: 'Ambulatory', code: 'AMB' },
{ value: '2', label: 'Inpatient', code: 'IMP' },
{ value: '3', label: 'Emergency', code: 'EMER' },
],
}
// Helper function untuk filter unit berdasarkan installation
export function getFilteredUnits(installationId: string) {
if (!installationId || !installationUnitMapping[installationId]) {
return []
}
const allowedUnitIds = installationUnitMapping[installationId]
return unitConf.items.filter((unit) => allowedUnitIds.includes(unit.value))
}
// Helper function untuk membuat unit config yang ter-filter
export function createFilteredUnitConf(installationId: string) {
return {
...unitConf,
items: getFilteredUnits(installationId),
}
}
@@ -0,0 +1,240 @@
<script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
import AppSpecialistEntryForm from '~/components/app/specialist/entry-form.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import Header from '~/components/pub/my-ui/nav-header/header.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { createFilteredUnitConf, installationConf, schemaConf, unitConf } from './entry'
// #region State & Computed
// Dialog state
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
// Table action rowId provider
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
// State untuk tracking installation yang dipilih di form
const selectedInstallationId = ref<string>('')
// Computed untuk filtered unit berdasarkan installation yang dipilih
const filteredUnitConf = computed(() => {
if (!selectedInstallationId.value) {
return { ...unitConf, items: [] }
}
return createFilteredUnitConf(selectedInstallationId.value)
})
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getSpecialistList,
} = usePaginatedList({
fetchFn: fetchSpecialistData,
entityName: 'specialist',
})
const headerPrep: HeaderPrep = {
title: 'Specialist',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (_val: string) => {
// Handle search input - this will be triggered by the header component
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
},
},
addNav: {
label: 'Tambah Specialist',
icon: 'i-lucide-send',
onClick: () => {
isFormEntryDialogOpen.value = true
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// #endregion
// #region Functions
async function fetchSpecialistData(params: any) {
const endpoint = transform('/api/v1/patient', params)
return await xfetch(endpoint)
}
async function handleDeleteRow(record: any) {
try {
// TODO : hit backend request untuk delete
console.log('Deleting record:', record)
// Simulate API call
// const response = await xfetch(`/api/v1/Installation/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getSpecialistList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
}
}
// Handle confirmation result
function handleConfirmDelete(record: any, action: string) {
console.log('Confirmed action:', action, 'for record:', record)
handleDeleteRow(record)
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion region
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
// Reset installation selection ketika form dibatal
selectedInstallationId.value = ''
setTimeout(() => {
resetForm()
}, 500)
}
function onInstallationChanged(installationId: string) {
// Update local state untuk trigger re-render filtered units
selectedInstallationId.value = installationId
// The filteredUnitConf computed will automatically update
// based on the new selectedInstallationId value
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/Installation', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Reset installation selection ketika form berhasil submit
selectedInstallationId.value = ''
// Refresh data after successful submission
await getSpecialistList()
// TODO: Show success message
console.log('Installation created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
}
}
// #endregion
// #region Watchers
// Watch for row actions
watch(recId, () => {
switch (recAction.value) {
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// Note: Installation change logic is now handled by the entry-form component
// through the onInstallationChanged event handler
// #endregion
</script>
<template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
<AppSpecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" :title="headerPrep.addNav?.label!" size="lg" prevent-outside>
<AppSpecialistEntryFormPrev
:installation="installationConf"
:unit="filteredUnitConf"
:schema="schemaConf"
:disabled-unit="!selectedInstallationId"
:initial-values="{ name: '', code: '', installationId: '', unitId: '' }"
@submit="onSubmitForm"
@cancel="onCancelForm"
@installation-changed="onInstallationChanged"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="handleConfirmDelete"
@cancel="handleCancelConfirmation"
>
<template #default="{ record }">
<div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p>
</div>
</template>
</RecordConfirmation>
</template>
<style scoped>
/* component style */
</style>
@@ -0,0 +1,98 @@
import * as z from 'zod'
export const schemaConf = z.object({
name: z
.string({
required_error: 'Nama spesialisasi harus diisi',
})
.min(3, 'Nama spesialisasi minimal 3 karakter'),
code: z
.string({
required_error: 'Kode spesialisasi harus diisi',
})
.min(3, 'Kode spesialisasi minimal 3 karakter'),
installationId: z
.string({
required_error: 'Instalasi harus dipilih',
})
.min(1, 'Instalasi harus dipilih'),
unitId: z
.string({
required_error: 'Unit harus dipilih',
})
.min(1, 'Unit harus dipilih'),
specialistId: z
.string({
required_error: 'Specialist harus dipilih',
})
.min(1, 'Specialist harus dipilih'),
})
// Unit mapping berdasarkan installation
export const installationUnitMapping: Record<string, string[]> = {
'1': ['1', '3'],
'2': ['2', '3'],
'3': ['1', '2', '3'],
}
export const unitConf = {
msg: {
placeholder: '---pilih unit',
search: 'kode, nama unit',
empty: 'unit tidak ditemukan',
},
items: [
{ value: '1', label: 'Instalasi Medis', code: 'MED' },
{ value: '2', label: 'Instalasi Keperawatan', code: 'NUR' },
{ value: '3', label: 'Instalasi Administrasi', code: 'ADM' },
],
}
export const specialistConf = {
msg: {
placeholder: '---pilih specialist',
search: 'kode, nama specialist',
empty: 'specialist tidak ditemukan',
},
items: [
{ value: '1', label: 'Spesialis Jantung', code: 'CARD' },
{ value: '2', label: 'Spesialis Mata', code: 'OPHT' },
{ value: '3', label: 'Spesialis Bedah', code: 'SURG' },
{ value: '4', label: 'Spesialis Anak', code: 'PEDI' },
{ value: '5', label: 'Spesialis Kandungan', code: 'OBGY' },
],
}
export const installationConf = {
msg: {
placeholder: '---pilih instalasi',
search: 'kode, nama instalasi',
empty: 'instalasi tidak ditemukan',
},
items: [
{ value: '1', label: 'Ambulatory', code: 'AMB' },
{ value: '2', label: 'Inpatient', code: 'IMP' },
{ value: '3', label: 'Emergency', code: 'EMER' },
],
}
// Helper function untuk filter unit berdasarkan installation
export function getFilteredUnits(installationId: string) {
if (!installationId || !installationUnitMapping[installationId]) {
return []
}
const allowedUnitIds = installationUnitMapping[installationId]
return unitConf.items.filter((unit) => allowedUnitIds.includes(unit.value))
}
// Helper function untuk membuat unit config yang ter-filter
export function createFilteredUnitConf(installationId: string) {
return {
...unitConf,
items: getFilteredUnits(installationId),
}
}
@@ -0,0 +1,239 @@
<script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
import AppSubspecialistEntryForm from '~/components/app/subspecialist/entry-form.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import Header from '~/components/pub/my-ui/nav-header/header.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { createFilteredUnitConf, installationConf, schemaConf, specialistConf, unitConf } from './entry'
// #region State & Computed
// Dialog state
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
// Table action rowId provider
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
// State untuk tracking installation, unit, dan specialist yang dipilih di form
const selectedInstallationId = ref<string>('')
const selectedUnitId = ref<string>('')
// Computed untuk filtered unit berdasarkan installation yang dipilih
const filteredUnitConf = computed(() => {
if (!selectedInstallationId.value) {
return { ...unitConf, items: [] }
}
return createFilteredUnitConf(selectedInstallationId.value)
})
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getSubSpecialistList,
} = usePaginatedList({
fetchFn: fetchSubSpecialistData,
entityName: 'subspecialist',
})
const headerPrep: HeaderPrep = {
title: 'Sub Specialist',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (_val: string) => {
// Handle search input - this will be triggered by the header component
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
},
},
addNav: {
label: 'Tambah Sub Specialist',
icon: 'i-lucide-send',
onClick: () => {
isFormEntryDialogOpen.value = true
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// #endregion
// #region Functions
async function fetchSubSpecialistData(params: any) {
const endpoint = transform('/api/v1/patient', params)
return await xfetch(endpoint)
}
async function handleDeleteRow(record: any) {
try {
// TODO : hit backend request untuk delete
console.log('Deleting record:', record)
// Simulate API call
// const response = await xfetch(`/api/v1/subspecialist/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getSubSpecialistList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
}
}
// Handle confirmation result
function handleConfirmDelete(record: any, action: string) {
console.log('Confirmed action:', action, 'for record:', record)
handleDeleteRow(record)
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion region
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
selectedInstallationId.value = ''
selectedUnitId.value = ''
setTimeout(() => {
resetForm()
}, 500)
}
function onInstallationChanged(installationId: string) {
selectedInstallationId.value = installationId
}
function onUnitChanged(unitId: string) {
selectedUnitId.value = unitId
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/subspecialist', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Reset selection ketika form berhasil submit
selectedInstallationId.value = ''
selectedUnitId.value = ''
// Refresh data after successful submission
await getSubSpecialistList()
// TODO: Show success message
console.log('Sub Specialist created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
}
}
// #endregion
// #region Watchers
// Watch for row actions
watch(recId, () => {
switch (recAction.value) {
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// #endregion
</script>
<template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
<AppSubspecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" :title="headerPrep.addNav?.label!" size="lg" prevent-outside>
<AppSubspecialistEntryFormPrev
:installation="installationConf"
:unit="filteredUnitConf"
:specialist="specialistConf"
:schema="schemaConf"
:initial-values="{ name: '', code: '', installationId: '', unitId: '', specialistId: '' }"
@submit="onSubmitForm"
@cancel="onCancelForm"
@installation-changed="onInstallationChanged"
@unit-changed="onUnitChanged"
/>
</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?.name"><strong>Nama:</strong> {{ record.name }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p>
<p v-if="record?.specialist"><strong>Specialist:</strong> {{ record.specialist?.name }}</p>
</div>
</template>
</RecordConfirmation>
</template>
+63
View File
@@ -0,0 +1,63 @@
import * as z from 'zod'
export const unitConf = {
msg: {
placeholder: '--- pilih instalasi',
search: 'kode, nama instalasi',
empty: 'instalasi tidak ditemukan',
},
items: [
{ value: '1', label: 'Instalasi Medis', code: 'MED' },
{ value: '2', label: 'Instalasi Keperawatan', code: 'NUR' },
{ value: '3', label: 'Instalasi Administrasi', code: 'ADM' },
{ value: '4', label: 'Instalasi Penunjang Non-Medis', code: 'SUP' },
{ value: '5', label: 'Instalasi Pendidikan & Pelatihan', code: 'EDU' },
{ value: '6', label: 'Instalasi Farmasi', code: 'PHA' },
{ value: '7', label: 'Instalasi Radiologi', code: 'RAD' },
{ value: '8', label: 'Instalasi Laboratorium', code: 'LAB' },
{ value: '9', label: 'Instalasi Keuangan', code: 'FIN' },
{ value: '10', label: 'Instalasi SDM', code: 'HR' },
{ value: '11', label: 'Instalasi Teknologi Informasi', code: 'ITS' },
{ value: '12', label: 'Instalasi Pemeliharaan & Sarana', code: 'MNT' },
{ value: '13', label: 'Instalasi Gizi / Catering', code: 'CAT' },
{ value: '14', label: 'Instalasi Keamanan', code: 'SEC' },
{ value: '15', label: 'Instalasi Gawat Darurat', code: 'EMR' },
{ value: '16', label: 'Instalasi Bedah Sentral', code: 'SUR' },
{ value: '17', label: 'Instalasi Rawat Jalan', code: 'OUT' },
{ value: '18', label: 'Instalasi Rawat Inap', code: 'INP' },
{ value: '19', label: 'Instalasi Rehabilitasi Medik', code: 'REB' },
{ value: '20', label: 'Instalasi Penelitian & Pengembangan', code: 'RSH' },
],
}
export const schemaConf = z.object({
name: z
.string({
required_error: 'Nama unit harus diisi',
})
.min(1, 'Nama unit harus diisi'),
code: z
.string({
required_error: 'Kode unit harus diisi',
})
.min(1, 'Kode unit harus diisi'),
parentId: z.preprocess(
(input: unknown) => {
if (typeof input === 'string') {
// Handle empty string case
if (input.trim() === '') {
return 0
}
return Number(input)
}
return input
},
z
.number({
required_error: 'Instalasi induk harus dipilih',
})
.refine((num) => num > 0, 'Instalasi induk harus dipilih'),
),
})
+207
View File
@@ -0,0 +1,207 @@
<script setup lang="ts">
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
import AppUnitEntryForm from '~/components/app/unit/entry-form.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import Header from '~/components/pub/my-ui/nav-header/header.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { schemaConf, unitConf } from './entry'
// #region State & Computed
// Dialog state
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
// Table action rowId provider
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
async function fetchUnitData(params: any) {
const endpoint = transform('/api/v1/patient', params)
return await xfetch(endpoint)
}
// Menggunakan composable untuk pagination
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getUnitList,
} = usePaginatedList({
fetchFn: fetchUnitData,
entityName: 'unit',
})
const headerPrep: HeaderPrep = {
title: 'Unit',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (_val: string) => {
// Handle search input - this will be triggered by the header component
},
onClick: () => {
// Handle search button click if needed
},
onClear: () => {
// Handle search clear
},
},
addNav: {
label: 'Tambah Unit',
icon: 'i-lucide-send',
onClick: () => {
isFormEntryDialogOpen.value = true
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// #endregion
// #region Functions
async function handleDeleteRow(record: any) {
try {
// TODO : hit backend request untuk delete
console.log('Deleting record:', record)
// Simulate API call
// const response = await xfetch(`/api/v1/unit/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getUnitList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
}
}
// #endregion region
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false
setTimeout(() => {
resetForm()
}, 500)
}
async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false
try {
// TODO: Implement form submission logic
console.log('Form submitted:', values)
// Simulate API call
// const response = await xfetch('/api/v1/unit', {
// method: 'POST',
// body: JSON.stringify(values)
// })
// If successful, mark as success and close dialog
isFormEntryDialogOpen.value = false
isSuccess = true
// Refresh data after successful submission
await getUnitList()
// TODO: Show success message
console.log('Unit created successfully')
} catch (error: unknown) {
console.warn('Error submitting form:', error)
isSuccess = false
// Don't close dialog or reset form on error
// TODO: Show error message to user
} finally {
if (isSuccess) {
setTimeout(() => {
resetForm()
}, 500)
}
}
}
// #endregion
// #region Watchers
// Watch for row actions
watch(recId, () => {
switch (recAction.value) {
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// Handle confirmation result
function handleConfirmDelete(record: any, action: string) {
console.log('Confirmed action:', action, 'for record:', record)
handleDeleteRow(record)
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion
</script>
<template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
<AppUnitList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" title="Tambah Unit" size="lg" prevent-outside>
<AppUnitEntryFormPrev
:unit="unitConf" :schema="schemaConf" :initial-values="{ name: '', code: '', parentId: '' }"
@submit="onSubmitForm" @cancel="onCancelForm"
/>
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation"
>
<template #default="{ record }">
<div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p>
</div>
</template>
</RecordConfirmation>
</template>
<style scoped>
/* component style */
</style>
+1 -1
View File
@@ -77,7 +77,7 @@ export function usePaginatedList<T = any>(options: UsePaginatedListOptions<T>) {
// Functions // Functions
async function fetchData() { async function fetchData() {
if (isLoading.isTableLoading) return if (isLoading.isTableLoading) return
isLoading.isTableLoading = true isLoading.isTableLoading = true
try { try {
+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 })
+24
View File
@@ -0,0 +1,24 @@
// Handlers
import { genCrudHandler } from '~/handlers/_handler'
// Services
import { create, update, remove } from '~/services/device-order-item.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
+24
View File
@@ -0,0 +1,24 @@
// Handlers
import { genCrudHandler } from '~/handlers/_handler'
// Services
import { create, update, remove } from '~/services/device-order-item.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
+20
View File
@@ -0,0 +1,20 @@
// Default item meta model for entities
export interface ItemMeta {
id: number
createdAt: string | null
deletedAt: string | null
updatedAt: string | null
}
// Pagination meta model for API responses
export interface PaginationMeta {
page_number: string
page_size: string
record_totalCount: string
source: string
}
export interface Base {
name: string
code: string
}
+47
View File
@@ -0,0 +1,47 @@
export interface Consultation {
id: number
encounter_id: number
date?: string
unit_id: number
doctor_id?: number
problem: string
solution?: string
repliedAt?: string
}
export interface CreateDto {
encounter_id: number
date: string
problem: string
dstUnit_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: '',
}
}
+1 -1
View File
@@ -13,4 +13,4 @@ export function genDevice(): Device {
name: '', name: '',
uom_code: '', uom_code: '',
} }
} }
+1 -1
View File
@@ -15,4 +15,4 @@ export function genDivision(): Division {
parent_id: null, parent_id: null,
childrens: null, childrens: null,
} }
} }
+1 -1
View File
@@ -15,4 +15,4 @@ export function genMaterial(): Material {
uom_code: '', uom_code: '',
stock: 0, stock: 0,
} }
} }
+1 -1
View File
@@ -13,4 +13,4 @@ export function genSpecialist(): Specialist {
name: '', name: '',
unit_id: 0 unit_id: 0
} }
} }
+1 -1
View File
@@ -13,4 +13,4 @@ export function genSubspecialist(): Subspecialist {
name: '', name: '',
specialist_id: 0 specialist_id: 0
} }
} }
+1 -1
View File
@@ -13,4 +13,4 @@ export function genUom(): Uom {
name: '', name: '',
erp_id: '', erp_id: '',
} }
} }
@@ -0,0 +1,9 @@
<script setup lang="ts">
definePageMeta({
roles: ['sys', 'doc'],
})
</script>
<template>
<div>detail pasien</div>
</template>
@@ -0,0 +1,9 @@
<script setup lang="ts">
definePageMeta({
roles: ['sys', 'doc'],
})
</script>
<template>
<div>edit pasien</div>
</template>
+41
View File
@@ -0,0 +1,41 @@
<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 Pasien',
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['/patient']
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 = hasCreateAccess(roleAccess)
</script>
<template>
<div v-if="canCreate">
<ContentPatientAdd />
</div>
<Error v-else :status-code="403" />
</template>
+40
View File
@@ -0,0 +1,40 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Daftar Pasien',
contentFrame: 'cf-full-width',
})
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)
</script>
<template>
<div>
<div v-if="canRead">
<ContentPatientList />
</div>
<Error v-else :status-code="403" />
</div>
</template>
+42
View File
@@ -0,0 +1,42 @@
<script setup lang="ts">
// import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/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>
+13
View File
@@ -0,0 +1,13 @@
import { z } from 'zod'
import type { CreateDto } from '~/models/consultation'
const ConsultationSchema = z.object({
date: z.string({ required_error: 'Tanggal harus diisi' }),
dstUnit_id: z.number({ required_error: 'Unit harus diisi' }),
problem: z.string({ required_error: 'Uraian harus diisi' }).min(20, 'Uraian minimum 20 karakter'),
})
type ConsultationFormData = z.infer<typeof ConsultationSchema> & (CreateDto)
export { ConsultationSchema }
export type { ConsultationFormData }
+12
View File
@@ -0,0 +1,12 @@
import { z } from 'zod'
import type { DeviceOrder } from '~/models/device-order'
const DeviceOrderSchema = z.object({
encounter_id: z.number({ required_error: 'Kode harus diisi' }),
doctor_id: z.number({ required_error: 'Kode harus diisi' }),
})
type DeviceOrderFormData = z.infer<typeof DeviceOrderSchema> & Partial<DeviceOrder>
export { DeviceOrderSchema }
export type { DeviceOrderFormData }
+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)
}
+26
View File
@@ -0,0 +1,26 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/device-order-item'
const name = 'device-order-item'
export function create(data: any) {
console.log('service create', data)
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)
}
+26
View File
@@ -0,0 +1,26 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/device-order'
const name = 'device-order'
export function create(data: any) {
console.log('service create', data)
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)
}
+1 -1
View File
@@ -38,4 +38,4 @@ export async function getValueLabelList(params: any = null): Promise<{ value: st
})) }))
} }
return data return data
} }
+163
View File
@@ -0,0 +1,163 @@
export default defineEventHandler(async (event) => {
// Ambil query parameters
const payload = { ...getQuery(event) }
const isTreeFormat = payload.tree === 'true' || payload.tree === '1'
// Mock data division dengan struktur nested meta parent
const baseDivisions = [
{
id: 1,
name: 'Direktorat Medis',
code: 'DIR-MED',
parentId: null,
},
{
id: 2,
name: 'Bidang Medik',
code: 'BDG-MED',
parentId: 1,
},
{
id: 3,
name: 'Tim Kerja Ranap, ICU, Bedah',
code: 'TIM-RAN',
parentId: 2,
},
{
id: 4,
name: 'Direktorat Keperawatan',
code: 'DIR-KEP',
parentId: null,
},
{
id: 5,
name: 'Bidang Keperawatan',
code: 'BDG-KEP',
parentId: 4,
},
{
id: 6,
name: 'Tim Kerja Keperawatan Ranap, ICU, Bedah',
code: 'TIM-KEP',
parentId: 5,
},
{
id: 7,
name: 'Direktorat Penunjang',
code: 'DIR-PNJ',
parentId: null,
},
{
id: 8,
name: 'Bidang Penunjang Medik',
code: 'BDG-PNJ',
parentId: 7,
},
{
id: 9,
name: 'Tim Kerja Radiologi',
code: 'TIM-RAD',
parentId: 8,
},
{
id: 10,
name: 'Direktorat Produksi',
code: 'DIR-PRD',
parentId: null,
},
{
id: 11,
name: 'Bidang Teknologi',
code: 'BDG-TEK',
parentId: 10,
},
{
id: 12,
name: 'Tim Kerja Software Engineering',
code: 'TIM-SWE',
parentId: 11,
},
{
id: 13,
name: 'Direktorat Operasional',
code: 'DIR-OPS',
parentId: null,
},
{
id: 14,
name: 'Bidang HR & GA',
code: 'BDG-HRG',
parentId: 13,
},
{
id: 15,
name: 'Tim Kerja Rekrutmen',
code: 'TIM-REC',
parentId: 14,
},
]
// Menambahkan meta parent pada setiap division
const divisions = baseDivisions
.map((division) => {
const parent = baseDivisions.find((d) => d.id === division.parentId)
const mapped = {
...division,
meta: {
parentId: parent?.id || null,
name: parent?.name || null,
code: parent?.code || null,
},
}
if (mapped.meta.parentId === null) {
mapped.meta = null
}
return mapped
})
.sort((a, b) => {
if (a.parentId === null && b.parentId !== null) return -1
if (a.parentId !== null && b.parentId === null) return 1
return a.id - b.id
})
// Jika tree format diminta, konversi ke struktur hierarki
if (isTreeFormat) {
const buildTree = (parentId = null) => {
return baseDivisions
.filter(division => division.parentId === parentId)
.map(division => ({
id: division.id,
name: division.name,
code: division.code,
children: buildTree(division.id),
}))
.sort((a, b) => a.id - b.id)
}
const treeData = buildTree()
return {
success: true,
data: treeData,
message: 'Data division dalam format tree berhasil diambil',
meta: {
record_totalCount: baseDivisions.length,
format: 'tree',
},
}
}
return {
success: true,
data: divisions,
message: 'Data division berhasil diambil',
meta: {
record_totalCount: divisions.length,
format: 'flat',
},
}
})
+171
View File
@@ -0,0 +1,171 @@
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Mock data lengkap
const mockData = [
{
id: 'RSC001',
resource_type: 'Encounter',
patient: {
name: 'Ahmad Wepe',
mrn: 'RM001234',
},
status: 2, // 0: failed, 1: pending, 2: success
updated_at: '2025-01-15',
fhir_id: 'ENC-00123',
},
{
id: 'RSC002',
resource_type: 'Patient',
patient: {
name: 'Siti Aminah',
mrn: 'RM001235',
},
status: 1,
updated_at: '2025-01-10',
fhir_id: 'PAT-001235',
},
{
id: 'RSC003',
resource_type: 'Observation',
patient: {
name: 'Budi Antono',
mrn: 'RM001236',
},
status: 0,
updated_at: '2025-01-11',
fhir_id: 'OBS-001236',
},
{
id: 'RSC004',
resource_type: 'Encounter',
patient: {
name: 'Maria Sari',
mrn: 'RM001237',
},
status: 2,
updated_at: '2025-01-12',
fhir_id: 'ENC-001237',
},
{
id: 'RSC005',
resource_type: 'Patient',
patient: {
name: 'Joko Widodo',
mrn: 'RM001238',
},
status: 1,
updated_at: '2025-01-13',
fhir_id: 'PAT-001238',
},
{
id: 'RSC006',
resource_type: 'Observation',
patient: {
name: 'Dewi Sartika',
mrn: 'RM001239',
},
status: 2,
updated_at: '2025-01-14',
fhir_id: 'OBS-001239',
},
{
id: 'RSC007',
resource_type: 'Encounter',
patient: {
name: 'Rudi Hartono',
mrn: 'RM001240',
},
status: 0,
updated_at: '2025-01-16',
fhir_id: 'ENC-001240',
},
{
id: 'RSC008',
resource_type: 'Patient',
patient: {
name: 'Sri Mulyani',
mrn: 'RM001241',
},
status: 2,
updated_at: '2025-01-17',
fhir_id: 'PAT-001241',
},
]
// Ekstrak parameter filter dari request body
const {
status,
resource_type,
date_from,
date_to,
search,
page = 1,
limit = 10,
} = body
let filteredData = [...mockData]
// Filter berdasarkan status
if (status !== undefined && status !== null && status !== '') {
filteredData = filteredData.filter(item => item.status === Number(status))
}
// Filter berdasarkan resource_type (transaction type)
if (resource_type && resource_type !== 'all') {
filteredData = filteredData.filter(item =>
item.resource_type.toLowerCase() === resource_type.toLowerCase(),
)
}
// Filter berdasarkan rentang tanggal
if (date_from) {
filteredData = filteredData.filter(item =>
new Date(item.updated_at) >= new Date(date_from),
)
}
if (date_to) {
filteredData = filteredData.filter(item =>
new Date(item.updated_at) <= new Date(date_to),
)
}
// Filter berdasarkan pencarian nama pasien atau MRN
if (search) {
filteredData = filteredData.filter(item =>
item.patient.name.toLowerCase().includes(search.toLowerCase())
|| item.patient.mrn.toLowerCase().includes(search.toLowerCase())
|| item.fhir_id.toLowerCase().includes(search.toLowerCase()),
)
}
// Pagination
const totalItems = filteredData.length
const totalPages = Math.ceil(totalItems / limit)
const offset = (page - 1) * limit
const paginatedData = filteredData.slice(offset, offset + limit)
// Simulasi delay untuk loading state
await new Promise(resolve => setTimeout(resolve, 300))
return {
success: true,
data: paginatedData,
meta: {
total: totalItems,
page: Number(page),
limit: Number(limit),
total_pages: totalPages,
has_next: page < totalPages,
has_prev: page > 1,
},
filters: {
status,
resource_type,
date_from,
date_to,
search,
},
}
})