feat(specialist): add installation-unit relationship to specialist form

- Implement installation-unit mapping and filtering logic
- Update entry form to handle installation selection and unit filtering
- Add installation and unit dropdowns with combobox components
This commit is contained in:
Khafid Prayoga
2025-09-12 14:42:05 +07:00
parent 41d87623e2
commit 87c36b94ea
3 changed files with 228 additions and 30 deletions

No files matched your search

+93 -7
View File
@@ -1,26 +1,52 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormErrors } from '~/types/error' import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod' import { toTypedSchema } from '@vee-validate/zod'
import Combobox from '~/components/pub/custom-ui/form/combobox.vue'
import FieldGroup from '~/components/pub/custom-ui/form/field-group.vue' import FieldGroup from '~/components/pub/custom-ui/form/field-group.vue'
import Field from '~/components/pub/custom-ui/form/field.vue' import Field from '~/components/pub/custom-ui/form/field.vue'
import Label from '~/components/pub/custom-ui/form/label.vue' import Label from '~/components/pub/custom-ui/form/label.vue'
import Select from '~/components/pub/custom-ui/form/select.vue'
interface SpecialistFormData { interface SpecialistFormData {
name: string name: string
code: string code: string
encounterClassCode: string installationId: string
unitId: string
} }
const props = defineProps<{ const props = defineProps<{
schema: any schema: any
initialValues?: Partial<SpecialistFormData> initialValues?: Partial<SpecialistFormData>
errors?: FormErrors 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<{ const emit = defineEmits<{
'submit': [values: SpecialistFormData, resetForm: () => void] 'submit': [values: SpecialistFormData, resetForm: () => void]
'cancel': [resetForm: () => void] 'cancel': [resetForm: () => void]
'installationChanged': [id: string]
}>() }>()
const formSchema = toTypedSchema(props.schema) const formSchema = toTypedSchema(props.schema)
@@ -30,7 +56,8 @@ function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: SpecialistFormData = { const formData: SpecialistFormData = {
name: values.name || '', name: values.name || '',
code: values.code || '', code: values.code || '',
encounterClassCode: values.encounterClassCode || '', installationId: values.installationId || '',
unitId: values.unitId || '',
} }
emit('submit', formData, resetForm) emit('submit', formData, resetForm)
} }
@@ -39,11 +66,17 @@ function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
function onCancelForm({ resetForm }: { resetForm: () => void }) { function onCancelForm({ resetForm }: { resetForm: () => void }) {
emit('cancel', resetForm) emit('cancel', resetForm)
} }
// Watch for installation changes
function onInstallationChanged(installationId: string, setFieldValue: any) {
setFieldValue('unitId', '', false)
emit('installationChanged', installationId || '')
}
</script> </script>
<template> <template>
<Form <Form
v-slot="{ handleSubmit, resetForm }" as="" keep-values :validation-schema="formSchema" v-slot="{ handleSubmit, resetForm, values, setFieldValue }" as="" keep-values :validation-schema="formSchema"
:initial-values="initialValues" :initial-values="initialValues"
> >
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))"> <form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
@@ -56,9 +89,12 @@ function onCancelForm({ resetForm }: { resetForm: () => void }) {
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input
id="name" type="text" placeholder="Masukkan nama instalasi" autocomplete="off" id="name"
type="text"
placeholder="Masukkan nama spesialisasi"
autocomplete="off"
v-bind="componentField" v-bind="componentField"
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -72,7 +108,57 @@ function onCancelForm({ resetForm }: { resetForm: () => void }) {
<FormField v-slot="{ componentField }" name="code"> <FormField v-slot="{ componentField }" name="code">
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input id="code" type="text" placeholder="Masukkan kode instalasi" autocomplete="off" v-bind="componentField" /> <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> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
+75 -1
View File
@@ -1,6 +1,6 @@
import * as z from 'zod' import * as z from 'zod'
export const schemaConf = z.object({ export const specialistConf = z.object({
name: z name: z
.string({ .string({
required_error: 'Nama spesialisasi harus diisi', required_error: 'Nama spesialisasi harus diisi',
@@ -13,9 +13,83 @@ export const schemaConf = z.object({
}) })
.min(3, 'Kode spesialisasi minimal 3 karakter'), .min(3, 'Kode spesialisasi minimal 3 karakter'),
installationId: z
.string({
required_error: 'Instalasi harus dipilih',
})
.min(1, 'Instalasi harus dipilih'),
unitId: z unitId: z
.string({ .string({
required_error: 'Unit harus dipilih', required_error: 'Unit harus dipilih',
}) })
.min(1, '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),
}
}
+60 -22
View File
@@ -6,7 +6,7 @@ import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-c
import { ActionEvents } from '~/components/pub/custom-ui/data/types' import { ActionEvents } from '~/components/pub/custom-ui/data/types'
import Header from '~/components/pub/custom-ui/nav-header/header.vue' import Header from '~/components/pub/custom-ui/nav-header/header.vue'
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
import { schemaConf } from './entry' import { createFilteredUnitConf, installationConf, specialistConf, unitConf } from './entry'
// #region State & Computed // #region State & Computed
// Dialog state // Dialog state
@@ -18,12 +18,17 @@ const recId = ref<number>(0)
const recAction = ref<string>('') const recAction = ref<string>('')
const recItem = ref<any>(null) const recItem = ref<any>(null)
async function fetchSpecialistData(params: any) { // State untuk tracking installation yang dipilih di form
const endpoint = transform('/api/v1/patient', params) const selectedInstallationId = ref<string>('')
return await xfetch(endpoint)
} // Computed untuk filtered unit berdasarkan installation yang dipilih
const filteredUnitConf = computed(() => {
if (!selectedInstallationId.value) {
return { ...unitConf, items: [] }
}
return createFilteredUnitConf(selectedInstallationId.value)
})
// Menggunakan composable untuk pagination
const { const {
data, data,
isLoading, isLoading,
@@ -71,6 +76,10 @@ provide('table_data_loader', isLoading)
// #endregion // #endregion
// #region Functions // #region Functions
async function fetchSpecialistData(params: any) {
const endpoint = transform('/api/v1/patient', params)
return await xfetch(endpoint)
}
async function handleDeleteRow(record: any) { async function handleDeleteRow(record: any) {
try { try {
@@ -98,17 +107,40 @@ async function handleDeleteRow(record: any) {
} }
} }
// 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 // #endregion region
// #region Form event handlers // #region Form event handlers
function onCancelForm(resetForm: () => void) { function onCancelForm(resetForm: () => void) {
isFormEntryDialogOpen.value = false isFormEntryDialogOpen.value = false
// Reset installation selection ketika form dibatal
selectedInstallationId.value = ''
setTimeout(() => { setTimeout(() => {
resetForm() resetForm()
}, 500) }, 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) { async function onSubmitForm(values: any, resetForm: () => void) {
let isSuccess = false let isSuccess = false
try { try {
@@ -125,6 +157,9 @@ async function onSubmitForm(values: any, resetForm: () => void) {
isFormEntryDialogOpen.value = false isFormEntryDialogOpen.value = false
isSuccess = true isSuccess = true
// Reset installation selection ketika form berhasil submit
selectedInstallationId.value = ''
// Refresh data after successful submission // Refresh data after successful submission
await getSpecialistList() await getSpecialistList()
@@ -161,18 +196,8 @@ watch(recId, () => {
} }
}) })
// Handle confirmation result // Note: Installation change logic is now handled by the entry-form component
function handleConfirmDelete(record: any, action: string) { // through the onInstallationChanged event handler
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 // #endregion
</script> </script>
@@ -182,12 +207,25 @@ function handleCancelConfirmation() {
<AppSpecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppSpecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isFormEntryDialogOpen" :title="headerPrep.addNav?.label!" size="lg" prevent-outside> <Dialog v-model:open="isFormEntryDialogOpen" :title="headerPrep.addNav?.label!" size="lg" prevent-outside>
<AppSpecialistEntryForm :schema="schemaConf" :initial-values="{ name: '', code: '', encounterClassCode: '' }" <AppSpecialistEntryForm
@submit="onSubmitForm" @cancel="onCancelForm" /> :installation="installationConf"
:unit="filteredUnitConf"
:schema="specialistConf"
:disabled-unit="!selectedInstallationId"
:initial-values="{ name: '', code: '', installationId: '', unitId: '' }"
@submit="onSubmitForm"
@cancel="onCancelForm"
@installation-changed="onInstallationChanged"
/>
</Dialog> </Dialog>
<RecordConfirmation v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem" <RecordConfirmation
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation"> v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="handleConfirmDelete"
@cancel="handleCancelConfirmation"
>
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p><strong>ID:</strong> {{ record?.id }}</p>