500 lines
15 KiB
Plaintext
500 lines
15 KiB
Plaintext
<script setup lang="ts">
|
|
// Components
|
|
import Block from '~/components/pub/my-ui/doc-entry/block.vue'
|
|
import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
|
|
import Field from '~/components/pub/my-ui/doc-entry/field.vue'
|
|
import Label from '~/components/pub/my-ui/doc-entry/label.vue'
|
|
import { Button } from '~/components/pub/ui/button'
|
|
import { Input } from '~/components/pub/ui/input'
|
|
import Select from '~/components/pub/ui/select/Select.vue'
|
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
|
import DatepickerSingle from '~/components/pub/my-ui/datepicker/datepicker-single.vue'
|
|
import TreeSelect from '~/components/pub/my-ui/select-tree/tree-select.vue'
|
|
import FileUpload from '~/components/pub/my-ui/form/file-field.vue'
|
|
|
|
// Types
|
|
import { IntegrationEncounterSchema, type IntegrationEncounterFormData } from '~/schemas/integration-encounter.schema'
|
|
import type { PatientEntity } from '~/models/patient'
|
|
import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
|
|
|
|
// Helpers
|
|
import { toTypedSchema } from '@vee-validate/zod'
|
|
import { useForm } from 'vee-validate'
|
|
import { refDebounced } from '@vueuse/core'
|
|
|
|
const props = defineProps<{
|
|
isLoading?: boolean
|
|
isReadonly?: boolean
|
|
isSepValid?: boolean
|
|
isCheckingSep?: boolean
|
|
doctor?: any[]
|
|
subSpecialist?: any[]
|
|
specialists?: TreeItem[]
|
|
payments: any[]
|
|
participantGroups?: any[]
|
|
seps: any[]
|
|
patient?: PatientEntity | null | undefined
|
|
objects?: any
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'event', menu: string, value?: any): void
|
|
(e: 'fetch', value?: any): void
|
|
}>()
|
|
|
|
// Validation schema
|
|
const { handleSubmit, errors, defineField, meta } = useForm<IntegrationEncounterFormData>({
|
|
validationSchema: toTypedSchema(IntegrationEncounterSchema),
|
|
})
|
|
|
|
// Bind fields and extract attrs
|
|
const [doctorId, doctorIdAttrs] = defineField('doctorId')
|
|
const [subSpecialistId, subSpecialistIdAttrs] = defineField('subSpecialistId')
|
|
const [registerDate, registerDateAttrs] = defineField('registerDate')
|
|
const [paymentType, paymentTypeAttrs] = defineField('paymentType')
|
|
const [patientCategory, patientCategoryAttrs] = defineField('patientCategory')
|
|
const [cardNumber, cardNumberAttrs] = defineField('cardNumber')
|
|
const [sepType, sepTypeAttrs] = defineField('sepType')
|
|
const [sepNumber, sepNumberAttrs] = defineField('sepNumber')
|
|
const [patientName, patientNameAttrs] = defineField('patientName')
|
|
const [nationalIdentity, nationalIdentityAttrs] = defineField('nationalIdentity')
|
|
const [medicalRecordNumber, medicalRecordNumberAttrs] = defineField('medicalRecordNumber')
|
|
const patientId = ref('')
|
|
|
|
const isLoading = props.isLoading !== undefined ? props.isLoading : false
|
|
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
|
|
|
|
// SEP validation state from props
|
|
const isSepValid = computed(() => props.isSepValid || false)
|
|
const isCheckingSep = computed(() => props.isCheckingSep || false)
|
|
|
|
const doctorOpts = computed(() => {
|
|
// Add default option
|
|
const defaultOption = [{ label: 'Pilih', value: '' }]
|
|
// Add doctors from props
|
|
const doctors = props.doctor || []
|
|
return [...defaultOption, ...doctors]
|
|
})
|
|
|
|
const isJKNPayment = computed(() => paymentType.value === 'jkn')
|
|
|
|
async function onFetchChildren(parentId: string): Promise<void> {
|
|
console.log('onFetchChildren', parentId)
|
|
}
|
|
|
|
// Watch specialist/subspecialist selection to fetch doctors
|
|
watch(subSpecialistId, async (newValue) => {
|
|
if (newValue) {
|
|
console.log('SubSpecialist changed:', newValue)
|
|
// Reset doctor selection
|
|
doctorId.value = ''
|
|
// Emit fetch event to parent
|
|
emit('fetch', { subSpecialistId: newValue })
|
|
}
|
|
})
|
|
|
|
// Debounced SEP number watcher: emit change only after user stops typing
|
|
const debouncedSepNumber = refDebounced(sepNumber, 500)
|
|
watch(debouncedSepNumber, (newValue) => {
|
|
emit('event', 'sep-number-changed', newValue)
|
|
})
|
|
|
|
// Sync props to form fields
|
|
watch(
|
|
() => props.objects,
|
|
(objects) => {
|
|
if (objects && Object.keys(objects).length > 0) {
|
|
patientName.value = objects?.patientName || ''
|
|
nationalIdentity.value = objects?.nationalIdentity || ''
|
|
medicalRecordNumber.value = objects?.medicalRecordNumber || ''
|
|
doctorId.value = objects?.doctorId || ''
|
|
subSpecialistId.value = objects?.subSpecialistId || ''
|
|
registerDate.value = objects?.registerDate || ''
|
|
paymentType.value = objects?.paymentType || ''
|
|
patientCategory.value = objects?.patientCategory || ''
|
|
cardNumber.value = objects?.cardNumber || ''
|
|
sepType.value = objects?.sepType || ''
|
|
sepNumber.value = objects?.sepNumber || ''
|
|
}
|
|
},
|
|
{ deep: true, immediate: true },
|
|
)
|
|
|
|
watch(
|
|
() => props.patient,
|
|
(patient) => {
|
|
if (patient && Object.keys(patient).length > 0) {
|
|
patientId.value = patient?.id ? String(patient.id) : ''
|
|
patientName.value = patient?.person?.name || ''
|
|
nationalIdentity.value = patient?.person?.residentIdentityNumber || ''
|
|
medicalRecordNumber.value = patient?.number || ''
|
|
}
|
|
},
|
|
{ deep: true, immediate: true },
|
|
)
|
|
|
|
function onAddSep() {
|
|
const formValues = {
|
|
patientId: patientId.value || '',
|
|
doctorCode: doctorId.value,
|
|
subSpecialistCode: subSpecialistId.value,
|
|
registerDate: registerDate.value,
|
|
cardNumber: cardNumber.value,
|
|
paymentType: paymentType.value,
|
|
sepType: sepType.value
|
|
}
|
|
emit('event', 'add-sep', formValues)
|
|
}
|
|
|
|
// Submit handler
|
|
const onSubmit = handleSubmit((values) => {
|
|
console.log('✅ Validated form values:', JSON.stringify(values, null, 2))
|
|
emit('event', 'save', values)
|
|
})
|
|
|
|
// Expose submit method for parent component
|
|
const formRef = ref<HTMLFormElement | null>(null)
|
|
|
|
function submitForm() {
|
|
console.log('🔵 submitForm called, formRef:', formRef.value)
|
|
console.log('🔵 Form values:', {
|
|
doctorId: doctorId.value,
|
|
subSpecialistId: subSpecialistId.value,
|
|
registerDate: registerDate.value,
|
|
paymentType: paymentType.value,
|
|
})
|
|
console.log('🔵 Form errors:', errors.value)
|
|
console.log('🔵 Form meta:', meta.value)
|
|
|
|
// Trigger form submit using native form submit
|
|
// This will trigger validation and onSubmit handler
|
|
if (formRef.value) {
|
|
console.log('🔵 Calling formRef.value.requestSubmit()')
|
|
formRef.value.requestSubmit()
|
|
} else {
|
|
console.warn('⚠️ formRef.value is null, cannot submit form')
|
|
// Fallback: directly call onSubmit handler
|
|
// Create a mock event object
|
|
const mockEvent = {
|
|
preventDefault: () => {},
|
|
target: formRef.value || {},
|
|
} as SubmitEvent
|
|
|
|
// Call onSubmit directly
|
|
console.log('🔵 Calling onSubmit with mock event')
|
|
onSubmit(mockEvent)
|
|
}
|
|
}
|
|
|
|
defineExpose({
|
|
submitForm,
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="mx-auto w-full">
|
|
<form
|
|
ref="formRef"
|
|
@submit.prevent="onSubmit"
|
|
class="grid gap-6 p-4"
|
|
>
|
|
<!-- Data Pasien -->
|
|
<div class="flex flex-col gap-2">
|
|
<h3 class="text-lg font-semibold">Data Pasien</h3>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm">sudah pernah terdaftar sebagai pasien?</span>
|
|
<Button
|
|
variant="outline"
|
|
type="button"
|
|
class="h-[40px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50"
|
|
@click="emit('event', 'search')"
|
|
>
|
|
<Icon
|
|
name="i-lucide-search"
|
|
class="h-5 w-5"
|
|
/>
|
|
Cari Pasien
|
|
</Button>
|
|
<span class="text-sm">belum pernah terdaftar sebagai pasien?</span>
|
|
<Button
|
|
variant="outline"
|
|
type="button"
|
|
class="h-[40px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50"
|
|
@click="emit('event', 'add')"
|
|
>
|
|
<Icon
|
|
name="i-lucide-plus"
|
|
class="h-5 w-5"
|
|
/>
|
|
Tambah Pasien Baru
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Block
|
|
labelSize="thin"
|
|
class="!pt-0"
|
|
:colCount="3"
|
|
:cellFlex="false"
|
|
>
|
|
<Cell>
|
|
<Label height="compact">Nama Pasien</Label>
|
|
<Field :errMessage="errors.patientName">
|
|
<Input
|
|
id="patientName"
|
|
v-model="patientName"
|
|
v-bind="patientNameAttrs"
|
|
:disabled="true"
|
|
/>
|
|
</Field>
|
|
</Cell>
|
|
|
|
<Cell>
|
|
<Label height="compact">NIK</Label>
|
|
<Field :errMessage="errors.nationalIdentity">
|
|
<Input
|
|
id="nationalIdentity"
|
|
v-model="nationalIdentity"
|
|
v-bind="nationalIdentityAttrs"
|
|
:disabled="true"
|
|
/>
|
|
</Field>
|
|
</Cell>
|
|
|
|
<Cell>
|
|
<Label height="compact">No. RM</Label>
|
|
<Field :errMessage="errors.medicalRecordNumber">
|
|
<Input
|
|
id="medicalRecordNumber"
|
|
v-model="medicalRecordNumber"
|
|
v-bind="medicalRecordNumberAttrs"
|
|
:disabled="true"
|
|
/>
|
|
</Field>
|
|
</Cell>
|
|
</Block>
|
|
|
|
<hr />
|
|
|
|
<!-- Data Kunjungan -->
|
|
<h3 class="text-lg font-semibold">Data Kunjungan</h3>
|
|
|
|
<Block
|
|
labelSize="thin"
|
|
class="!pt-0"
|
|
:colCount="3"
|
|
:cellFlex="false"
|
|
>
|
|
<Cell>
|
|
<Label height="compact">
|
|
Dokter
|
|
<span class="text-red-500">*</span>
|
|
</Label>
|
|
<Field :errMessage="errors.doctorId">
|
|
<Combobox
|
|
id="doctorId"
|
|
v-model="doctorId"
|
|
v-bind="doctorIdAttrs"
|
|
:items="doctorOpts"
|
|
:is-disabled="isLoading || isReadonly"
|
|
placeholder="Pilih Dokter"
|
|
search-placeholder="Cari Dokter"
|
|
empty-message="Dokter tidak ditemukan"
|
|
/>
|
|
</Field>
|
|
</Cell>
|
|
|
|
<Cell>
|
|
<Label height="compact">
|
|
Spesialis / Subspesialis
|
|
<span class="text-red-500">*</span>
|
|
</Label>
|
|
<Field :errMessage="errors.subSpecialistId">
|
|
<TreeSelect
|
|
id="subSpecialistId"
|
|
v-model="subSpecialistId"
|
|
v-bind="subSpecialistIdAttrs"
|
|
:data="specialists || []"
|
|
:on-fetch-children="onFetchChildren"
|
|
/>
|
|
</Field>
|
|
</Cell>
|
|
</Block>
|
|
|
|
<Block
|
|
labelSize="thin"
|
|
class="!pt-0"
|
|
:colCount="3"
|
|
:cellFlex="false"
|
|
>
|
|
<Cell>
|
|
<Label height="compact">
|
|
Tanggal Daftar
|
|
<span class="text-red-500">*</span>
|
|
</Label>
|
|
<Field :errMessage="errors.registerDate">
|
|
<DatepickerSingle
|
|
id="registerDate"
|
|
v-model="registerDate"
|
|
v-bind="registerDateAttrs"
|
|
placeholder="Pilih tanggal"
|
|
/>
|
|
</Field>
|
|
</Cell>
|
|
|
|
<Cell>
|
|
<Label height="compact">
|
|
Jenis Pembayaran
|
|
<span class="text-red-500">*</span>
|
|
</Label>
|
|
<Field :errMessage="errors.paymentType">
|
|
<Select
|
|
id="paymentType"
|
|
v-model="paymentType"
|
|
v-bind="paymentTypeAttrs"
|
|
:items="payments"
|
|
:disabled="isLoading || isReadonly"
|
|
placeholder="Pilih Jenis Pembayaran"
|
|
/>
|
|
</Field>
|
|
</Cell>
|
|
</Block>
|
|
|
|
<!-- BPJS Fields (conditional) -->
|
|
<template v-if="isJKNPayment">
|
|
<Block
|
|
labelSize="thin"
|
|
class="!pt-0"
|
|
:colCount="3"
|
|
:cellFlex="false"
|
|
>
|
|
<Cell>
|
|
<Label height="compact">
|
|
Kelompok Peserta
|
|
<span class="text-red-500">*</span>
|
|
</Label>
|
|
<Field :errMessage="errors.patientCategory">
|
|
<Select
|
|
id="patientCategory"
|
|
v-model="patientCategory"
|
|
v-bind="patientCategoryAttrs"
|
|
:items="participantGroups || []"
|
|
:disabled="isLoading || isReadonly"
|
|
placeholder="Pilih Kelompok Peserta"
|
|
/>
|
|
</Field>
|
|
</Cell>
|
|
|
|
<Cell>
|
|
<Label height="compact">
|
|
No. Kartu BPJS
|
|
<span class="text-red-500">*</span>
|
|
</Label>
|
|
<Field :errMessage="errors.cardNumber">
|
|
<Input
|
|
id="cardNumber"
|
|
v-model="cardNumber"
|
|
v-bind="cardNumberAttrs"
|
|
:disabled="isLoading || isReadonly"
|
|
placeholder="Masukkan nomor kartu BPJS"
|
|
/>
|
|
</Field>
|
|
</Cell>
|
|
|
|
<Cell>
|
|
<Label height="compact">
|
|
Jenis SEP
|
|
<span class="text-red-500">*</span>
|
|
</Label>
|
|
<Field :errMessage="errors.sepType">
|
|
<Select
|
|
id="sepType"
|
|
v-model="sepType"
|
|
v-bind="sepTypeAttrs"
|
|
:items="seps"
|
|
:disabled="isLoading || isReadonly"
|
|
placeholder="Pilih Jenis SEP"
|
|
/>
|
|
</Field>
|
|
</Cell>
|
|
</Block>
|
|
|
|
<Block
|
|
labelSize="thin"
|
|
class="!pt-0"
|
|
:colCount="3"
|
|
:cellFlex="false"
|
|
>
|
|
<Cell>
|
|
<Label height="compact">
|
|
No. SEP
|
|
<span class="text-red-500">*</span>
|
|
</Label>
|
|
<Field :errMessage="errors.sepNumber">
|
|
<div class="flex gap-2">
|
|
<Input
|
|
id="sepNumber"
|
|
v-model="sepNumber"
|
|
v-bind="sepNumberAttrs"
|
|
placeholder="Tambah SEP terlebih dahulu"
|
|
class="flex-1"
|
|
:disabled="isLoading || isReadonly"
|
|
/>
|
|
<Button
|
|
v-if="!isSepValid"
|
|
variant="outline"
|
|
type="button"
|
|
class="bg-primary"
|
|
size="sm"
|
|
:disabled="isCheckingSep || isLoading || isReadonly"
|
|
@click="onAddSep"
|
|
>
|
|
<Icon
|
|
v-if="isCheckingSep"
|
|
name="i-lucide-loader-2"
|
|
class="h-4 w-4 animate-spin"
|
|
/>
|
|
<Icon
|
|
v-else
|
|
name="i-lucide-plus"
|
|
class="h-4 w-4"
|
|
/>
|
|
</Button>
|
|
<Button
|
|
v-else
|
|
variant="outline"
|
|
type="button"
|
|
class="bg-green-500 text-white hover:bg-green-600"
|
|
size="sm"
|
|
disabled
|
|
>
|
|
<Icon
|
|
name="i-lucide-check"
|
|
class="h-4 w-4"
|
|
/>
|
|
</Button>
|
|
</div>
|
|
</Field>
|
|
</Cell>
|
|
|
|
<FileUpload
|
|
field-name="sepFile"
|
|
label="Dokumen SEP"
|
|
placeholder="Unggah dokumen SEP"
|
|
:accept="['pdf', 'jpg', 'png']"
|
|
:max-size-mb="1"
|
|
/>
|
|
|
|
<FileUpload
|
|
field-name="sippFile"
|
|
label="Dokumen SIPP"
|
|
placeholder="Unggah dokumen SIPP"
|
|
:accept="['pdf', 'jpg', 'png']"
|
|
:max-size-mb="1"
|
|
/>
|
|
</Block>
|
|
</template>
|
|
</form>
|
|
</div>
|
|
</template>
|