feat(encounter): enhance entry form with submit method and save functionality
This commit is contained in:
@@ -131,11 +131,25 @@ const onSubmit = handleSubmit((values) => {
|
|||||||
console.log('✅ Validated form values:', JSON.stringify(values, null, 2))
|
console.log('✅ Validated form values:', JSON.stringify(values, null, 2))
|
||||||
emit('event', 'save', values)
|
emit('event', 'save', values)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Expose submit method for parent component
|
||||||
|
const formRef = ref<HTMLFormElement | null>(null)
|
||||||
|
|
||||||
|
function submitForm() {
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.requestSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
submitForm,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mx-auto w-full">
|
<div class="mx-auto w-full">
|
||||||
<form
|
<form
|
||||||
|
ref="formRef"
|
||||||
@submit.prevent="onSubmit"
|
@submit.prevent="onSubmit"
|
||||||
class="grid gap-6 p-4"
|
class="grid gap-6 p-4"
|
||||||
>
|
>
|
||||||
@@ -411,34 +425,6 @@ const onSubmit = handleSubmit((values) => {
|
|||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="mt-6 flex justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
type="button"
|
|
||||||
class="h-[40px] min-w-[120px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50 hover:text-orange-400"
|
|
||||||
@click="emit('event', 'cancel')"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="i-lucide-x"
|
|
||||||
class="h-5 w-5"
|
|
||||||
/>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
class="h-[40px] min-w-[120px] text-white"
|
|
||||||
:disabled="isLoading"
|
|
||||||
@click="onSubmit"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="i-lucide-save"
|
|
||||||
class="h-5 w-5"
|
|
||||||
/>
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Components
|
// Components
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
import { Button } from '~/components/pub/ui/button'
|
||||||
import AppEncounterEntryForm from '~/components/app/encounter/entry-form.vue'
|
import AppEncounterEntryForm from '~/components/app/encounter/entry-form.vue'
|
||||||
import AppViewPatient from '~/components/app/patient/view-patient.vue'
|
import AppViewPatient from '~/components/app/patient/view-patient.vue'
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ import {
|
|||||||
getValueTreeItems as getSpecialistTreeItems,
|
getValueTreeItems as getSpecialistTreeItems,
|
||||||
} from '~/services/specialist.service'
|
} from '~/services/specialist.service'
|
||||||
import { getValueLabelList as getDoctorValueLabelList } from '~/services/doctor.service'
|
import { getValueLabelList as getDoctorValueLabelList } from '~/services/doctor.service'
|
||||||
|
import { create as createEncounter } from '~/services/encounter.service'
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +31,11 @@ import {
|
|||||||
getPatientByIdentifierSearch,
|
getPatientByIdentifierSearch,
|
||||||
} from '~/handlers/patient.handler'
|
} from '~/handlers/patient.handler'
|
||||||
|
|
||||||
|
// Stores
|
||||||
|
import { useUserStore } from '~/stores/user'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
id: number
|
id: number
|
||||||
classCode?: 'ambulatory' | 'emergency' | 'inpatient'
|
classCode?: 'ambulatory' | 'emergency' | 'inpatient'
|
||||||
@@ -36,6 +44,7 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const userStore = useUserStore()
|
||||||
const openPatient = ref(false)
|
const openPatient = ref(false)
|
||||||
const isLoading = reactive<DataTableLoader>({
|
const isLoading = reactive<DataTableLoader>({
|
||||||
isTableLoading: false,
|
isTableLoading: false,
|
||||||
@@ -44,8 +53,16 @@ const paymentsList = ref<Array<{ value: string; label: string }>>([])
|
|||||||
const sepsList = ref<Array<{ value: string; label: string }>>([])
|
const sepsList = ref<Array<{ value: string; label: string }>>([])
|
||||||
const participantGroupsList = ref<Array<{ value: string; label: string }>>([])
|
const participantGroupsList = ref<Array<{ value: string; label: string }>>([])
|
||||||
const specialistsTree = ref<TreeItem[]>([])
|
const specialistsTree = ref<TreeItem[]>([])
|
||||||
|
const specialistsData = ref<any[]>([]) // Store full specialist data with id
|
||||||
const doctorsList = ref<Array<{ value: string; label: string }>>([])
|
const doctorsList = ref<Array<{ value: string; label: string }>>([])
|
||||||
const recSelectId = ref<number | null>(null)
|
const recSelectId = ref<number | null>(null)
|
||||||
|
const isSaving = ref(false)
|
||||||
|
const formRef = ref<InstanceType<typeof AppEncounterEntryForm> | null>(null)
|
||||||
|
|
||||||
|
// Computed for save button disable state
|
||||||
|
const isSaveDisabled = computed(() => {
|
||||||
|
return !selectedPatient.value || !selectedPatientObject.value || isSaving.value
|
||||||
|
})
|
||||||
|
|
||||||
function handleSavePatient() {
|
function handleSavePatient() {
|
||||||
selectedPatientObject.value = null
|
selectedPatientObject.value = null
|
||||||
@@ -77,7 +94,104 @@ function toNavigateSep(values: any) {
|
|||||||
navigateTo('/integration/bpjs/sep/add' + `?${queryParams.toString()}`)
|
navigateTo('/integration/bpjs/sep/add' + `?${queryParams.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEvent(menu: string, value?: any) {
|
async function handleSaveEncounter(formValues: any) {
|
||||||
|
// Validate patient is selected
|
||||||
|
if (!selectedPatient.value || !selectedPatientObject.value) {
|
||||||
|
toast({
|
||||||
|
title: 'Gagal',
|
||||||
|
description: 'Pasien harus dipilih terlebih dahulu',
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isSaving.value = true
|
||||||
|
|
||||||
|
// Get employee_id from user store
|
||||||
|
const employeeId = userStore.user?.employee_id || userStore.user?.employee?.id || 0
|
||||||
|
|
||||||
|
// Format date to ISO format
|
||||||
|
const formatDate = (dateString: string): string => {
|
||||||
|
if (!dateString) return ''
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get specialist_id and subspecialist_id from TreeSelect value (code)
|
||||||
|
const { specialist_id, subspecialist_id } = getSpecialistIdsFromCode(formValues.subSpecialistId || '')
|
||||||
|
|
||||||
|
// Build payload
|
||||||
|
const payload: any = {
|
||||||
|
patient_id: selectedPatientObject.value?.id || Number(selectedPatient.value),
|
||||||
|
registeredAt: formatDate(formValues.registerDate),
|
||||||
|
visitDate: formatDate(formValues.registerDate),
|
||||||
|
class_code: props.classCode || '',
|
||||||
|
subClass_code: props.subClassCode || '',
|
||||||
|
infra_id: 0,
|
||||||
|
unit_id: 0,
|
||||||
|
appointment_doctor_id: Number(formValues.doctorId),
|
||||||
|
responsible_doctor_id: Number(formValues.doctorId),
|
||||||
|
paymentType: formValues.paymentType,
|
||||||
|
cardNumber: formValues.cardNumber,
|
||||||
|
adm_employee_id: employeeId,
|
||||||
|
refSource_name: '',
|
||||||
|
appointment_id: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add specialist_id and subspecialist_id if available
|
||||||
|
if (specialist_id) {
|
||||||
|
payload.specialist_id = specialist_id
|
||||||
|
}
|
||||||
|
if (subspecialist_id) {
|
||||||
|
payload.subspecialist_id = subspecialist_id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formValues.paymentType === 'jkn') {
|
||||||
|
payload.paymentMethod_code = 'insurance'
|
||||||
|
payload.insuranceCompany_id = 0
|
||||||
|
payload.member_number = formValues.cardNumber
|
||||||
|
payload.ref_number = formValues.sepNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add visitMode_code and allocatedVisitCount only if classCode is ambulatory
|
||||||
|
if (props.classCode === 'ambulatory') {
|
||||||
|
payload.visitMode_code = 'adm'
|
||||||
|
payload.allocatedVisitCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call encounter service
|
||||||
|
const result = await createEncounter(payload)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast({
|
||||||
|
title: 'Berhasil',
|
||||||
|
description: 'Kunjungan berhasil dibuat',
|
||||||
|
variant: 'default',
|
||||||
|
})
|
||||||
|
// Optionally navigate or reset form
|
||||||
|
// navigateTo(`/encounter/${result.body?.data?.id}`)
|
||||||
|
} else {
|
||||||
|
const errorMessage = result.body?.message || 'Gagal membuat kunjungan'
|
||||||
|
toast({
|
||||||
|
title: 'Gagal',
|
||||||
|
description: errorMessage,
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error saving encounter:', error)
|
||||||
|
toast({
|
||||||
|
title: 'Gagal',
|
||||||
|
description: error?.message || 'Gagal membuat kunjungan',
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEvent(menu: string, value?: any) {
|
||||||
if (menu === 'search') {
|
if (menu === 'search') {
|
||||||
getPatientsList({ 'page-size': 10, includes: 'person' }).then(() => {
|
getPatientsList({ 'page-size': 10, includes: 'person' }).then(() => {
|
||||||
openPatient.value = true
|
openPatient.value = true
|
||||||
@@ -93,7 +207,7 @@ function handleEvent(menu: string, value?: any) {
|
|||||||
...value,
|
...value,
|
||||||
})
|
})
|
||||||
} else if (menu === 'save') {
|
} else if (menu === 'save') {
|
||||||
console.log('Save encounter:', value)
|
await handleSaveEncounter(value)
|
||||||
} else if (menu === 'cancel') {
|
} else if (menu === 'cancel') {
|
||||||
console.log('Cancel')
|
console.log('Cancel')
|
||||||
}
|
}
|
||||||
@@ -104,6 +218,7 @@ async function handleFetchSpecialists() {
|
|||||||
const specialistsResult = await getSpecialistList({ 'page-size': 100, includes: 'subspecialists' })
|
const specialistsResult = await getSpecialistList({ 'page-size': 100, includes: 'subspecialists' })
|
||||||
if (specialistsResult.success) {
|
if (specialistsResult.success) {
|
||||||
const specialists = specialistsResult.body?.data || []
|
const specialists = specialistsResult.body?.data || []
|
||||||
|
specialistsData.value = specialists // Store full data for mapping
|
||||||
specialistsTree.value = getSpecialistTreeItems(specialists)
|
specialistsTree.value = getSpecialistTreeItems(specialists)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -134,6 +249,47 @@ function isSubspecialist(value: string, items: TreeItem[]): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to find specialist_id and subspecialist_id from TreeSelect value (code)
|
||||||
|
* Returns { specialist_id: number | null, subspecialist_id: number | null }
|
||||||
|
*/
|
||||||
|
function getSpecialistIdsFromCode(code: string): { specialist_id: number | null; subspecialist_id: number | null } {
|
||||||
|
if (!code) {
|
||||||
|
return { specialist_id: null, subspecialist_id: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a subspecialist
|
||||||
|
const isSub = isSubspecialist(code, specialistsTree.value)
|
||||||
|
|
||||||
|
if (isSub) {
|
||||||
|
// Find subspecialist and its parent specialist
|
||||||
|
for (const specialist of specialistsData.value) {
|
||||||
|
if (specialist.subspecialists && Array.isArray(specialist.subspecialists)) {
|
||||||
|
for (const subspecialist of specialist.subspecialists) {
|
||||||
|
if (subspecialist.code === code) {
|
||||||
|
return {
|
||||||
|
specialist_id: specialist.id ? Number(specialist.id) : null,
|
||||||
|
subspecialist_id: subspecialist.id ? Number(subspecialist.id) : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's a specialist
|
||||||
|
for (const specialist of specialistsData.value) {
|
||||||
|
if (specialist.code === code) {
|
||||||
|
return {
|
||||||
|
specialist_id: specialist.id ? Number(specialist.id) : null,
|
||||||
|
subspecialist_id: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { specialist_id: null, subspecialist_id: null }
|
||||||
|
}
|
||||||
|
|
||||||
async function handleFetchDoctors(subSpecialistId: string | null = null) {
|
async function handleFetchDoctors(subSpecialistId: string | null = null) {
|
||||||
try {
|
try {
|
||||||
// Build filter based on selection type
|
// Build filter based on selection type
|
||||||
@@ -207,6 +363,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppEncounterEntryForm
|
<AppEncounterEntryForm
|
||||||
|
ref="formRef"
|
||||||
:payments="paymentsList"
|
:payments="paymentsList"
|
||||||
:seps="sepsList"
|
:seps="sepsList"
|
||||||
:participant-groups="participantGroupsList"
|
:participant-groups="participantGroupsList"
|
||||||
@@ -235,4 +392,32 @@ onMounted(async () => {
|
|||||||
"
|
"
|
||||||
@save="handleSavePatient"
|
@save="handleSavePatient"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Footer Actions -->
|
||||||
|
<div class="mt-6 flex justify-end gap-2 border-t border-t-slate-300 pt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
class="h-[40px] min-w-[120px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50 hover:text-orange-400"
|
||||||
|
@click="handleEvent('cancel')"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="i-lucide-x"
|
||||||
|
class="h-5 w-5"
|
||||||
|
/>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
class="h-[40px] min-w-[120px] text-white"
|
||||||
|
:disabled="isSaveDisabled"
|
||||||
|
@click="formRef?.submitForm()"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="i-lucide-save"
|
||||||
|
class="h-5 w-5"
|
||||||
|
/>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user