Merge branch 'dev' into feat/org-position-134
This commit is contained in:
@@ -1,354 +1,493 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
// 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 { Form } from '~/components/pub/ui/form'
|
||||
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'
|
||||
|
||||
import { educationCodes, genderCodes, occupationCodes, religionCodes, relationshipCodes } from '~/lib/constants'
|
||||
import { mapToComboboxOptList } from '~/lib/utils'
|
||||
// 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'
|
||||
|
||||
interface DivisionFormData {
|
||||
name: string
|
||||
code: string
|
||||
parentId: string
|
||||
}
|
||||
// Helpers
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
|
||||
const props = defineProps<{
|
||||
division: {
|
||||
msg: {
|
||||
placeholder: string
|
||||
search: string
|
||||
empty: string
|
||||
}
|
||||
}
|
||||
items: {
|
||||
value: string
|
||||
label: string
|
||||
code: string
|
||||
}[]
|
||||
schema: any
|
||||
initialValues?: Partial<DivisionFormData>
|
||||
errors?: FormErrors
|
||||
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<{
|
||||
submit: [values: DivisionFormData, resetForm: () => void]
|
||||
cancel: [resetForm: () => void]
|
||||
click: (e: Event) => void
|
||||
(e: 'event', menu: string, value?: any): void
|
||||
(e: 'fetch', value?: any): void
|
||||
}>()
|
||||
|
||||
const relationshipOpts = mapToComboboxOptList(relationshipCodes)
|
||||
const educationOpts = mapToComboboxOptList(educationCodes)
|
||||
const occupationOpts = mapToComboboxOptList(occupationCodes)
|
||||
const genderOpts = mapToComboboxOptList(genderCodes)
|
||||
// Validation schema
|
||||
const { handleSubmit, errors, defineField, meta } = useForm<IntegrationEncounterFormData>({
|
||||
validationSchema: toTypedSchema(IntegrationEncounterSchema),
|
||||
})
|
||||
|
||||
const formSchema = toTypedSchema(props.schema)
|
||||
// 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('')
|
||||
|
||||
// Form submission handler
|
||||
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
|
||||
const formData: DivisionFormData = {
|
||||
name: values.name || '',
|
||||
code: values.code || '',
|
||||
parentId: values.parentId || '',
|
||||
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 })
|
||||
}
|
||||
emit('submit', formData, resetForm)
|
||||
}
|
||||
const doctorOpts = ref([
|
||||
{ label: 'Pilih', value: null },
|
||||
{ label: 'Dr. A', value: 1 },
|
||||
])
|
||||
const paymentOpts = ref([
|
||||
{ label: 'Umum', value: 'umum' },
|
||||
{ label: 'BPJS', value: 'bpjs' },
|
||||
])
|
||||
const sepOpts = ref([
|
||||
{ label: 'Rujukan Internal', value: 'ri' },
|
||||
{ label: 'SEP Rujukan', value: 'sr' },
|
||||
])
|
||||
})
|
||||
|
||||
// file refs untuk tombol "Pilih Berkas"
|
||||
const sepFileInput = ref<HTMLInputElement | null>(null)
|
||||
const sippFileInput = ref<HTMLInputElement | null>(null)
|
||||
// Watch SEP number changes to notify parent
|
||||
watch(sepNumber, (newValue) => {
|
||||
emit('event', 'sep-number-changed', newValue)
|
||||
})
|
||||
|
||||
function pickSepFile() {
|
||||
sepFileInput.value?.click()
|
||||
}
|
||||
function pickSippFile() {
|
||||
sippFileInput.value?.click()
|
||||
}
|
||||
// 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 },
|
||||
)
|
||||
|
||||
function onSepFileChange(e: Event) {
|
||||
const f = (e.target as HTMLInputElement).files?.[0]
|
||||
// set ke form / emit / simpan di state sesuai form library-mu
|
||||
console.log('sep file', f)
|
||||
}
|
||||
|
||||
function onSippFileChange(e: Event) {
|
||||
const f = (e.target as HTMLInputElement).files?.[0]
|
||||
console.log('sipp file', f)
|
||||
}
|
||||
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() {
|
||||
// contoh handler tombol "+" di sebelah No. SEP
|
||||
console.log('open modal tambah SEP')
|
||||
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>
|
||||
<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="flex flex-col justify-between">
|
||||
<div class="mb-2 2xl:mb-3 text-sm 2xl:text-base font-semibold">
|
||||
Data Pasien
|
||||
<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 class="flex gap-6 mb-2 2xl:mb-2">
|
||||
<span>
|
||||
Sudah pernah terdaftar sebagai pasien?
|
||||
<Button class="bg-primary" size="sm" @click.prevent="emit('click', 'search')">
|
||||
<Icon name="i-lucide-search" class="mr-1" /> Cari Pasien
|
||||
</Button>
|
||||
</span>
|
||||
<span>
|
||||
Belum pernah terdaftar sebagai pasien?
|
||||
<Button class="bg-primary" size="sm" @click.prevent="emit('click', 'add')">
|
||||
<Icon name="i-lucide-plus" class="mr-1" /> Tambah Pasien Baru
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
<Block :colCount="3">
|
||||
<Cell>
|
||||
<Label label-for="patient_name">Nama Pasien</Label>
|
||||
<Field id="patient_name" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="patient_name">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="patient_name"
|
||||
v-bind="componentField"
|
||||
disabled
|
||||
placeholder="Tambah data pasien terlebih dahulu"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
|
||||
<!-- NIK -->
|
||||
<Cell :cosSpan="3">
|
||||
<Label label-for="nik">NIK</Label>
|
||||
<Field id="nik" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="nik">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input id="nik" v-bind="componentField" disabled placeholder="Otomatis" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Label label-for="rm">No. RM</Label>
|
||||
<Field id="rm" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="rm">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input id="rm" v-bind="componentField" disabled placeholder="RM99222" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
</Block>
|
||||
|
||||
<Separator class="my-4 2xl:my-5" />
|
||||
|
||||
<div class="mb-2 2xl:mb-3 text-sm 2xl:text-base font-semibold">
|
||||
Data Kunjungan
|
||||
</div>
|
||||
<Block :colCount="3">
|
||||
<!-- Dokter (Combobox) -->
|
||||
<Cell :cosSpan="3">
|
||||
<Label label-for="doctor_id">Dokter</Label>
|
||||
<Field id="doctor_id" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="doctor_id">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Combobox id="doctor_id" v-bind="componentField" :items="doctorOpts" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
|
||||
<!-- Tanggal Daftar (DatePicker) -->
|
||||
<Cell :cosSpan="3">
|
||||
<Label label-for="register_date">Tanggal Daftar</Label>
|
||||
<Field id="register_date" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="register_date">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<DatepickerSingle v-bind="componentField" placeholder="Pilih tanggal" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
|
||||
<!-- Jenis Pembayaran (Combobox) -->
|
||||
<Cell :cosSpan="3">
|
||||
<Label label-for="payment_type">Jenis Pembayaran</Label>
|
||||
<Field id="payment_type" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="payment_type">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<!-- <Combobox id="payment_type" v-bind="componentField" :items="paymentOpts" /> -->
|
||||
<Select id="payment_type" v-bind="componentField" :items="paymentOpts" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
</Block>
|
||||
|
||||
<Block :colCount="3">
|
||||
<Cell :cosSpan="3">
|
||||
<Label label-for="bpjs_number">Kelompok Peserta</Label>
|
||||
<Field id="bpjs_number" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="bpjs_number">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="bpjs_number"
|
||||
v-bind="componentField"
|
||||
placeholder="Pilih jenis pembayaran terlebih dahulu"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
|
||||
<!-- No. Kartu BPJS -->
|
||||
<Cell :cosSpan="3">
|
||||
<Label label-for="bpjs_number">No. Kartu BPJS</Label>
|
||||
<Field id="bpjs_number" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="bpjs_number">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="bpjs_number"
|
||||
v-bind="componentField"
|
||||
placeholder="Pilih jenis pembayaran terlebih dahulu"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
|
||||
<!-- Jenis SEP -->
|
||||
<Cell :cosSpan="3">
|
||||
<Label label-for="sep_type">Jenis SEP</Label>
|
||||
<Field id="sep_type" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="sep_type">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select id="sep_type" v-bind="componentField" :items="sepOpts" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
</Block>
|
||||
|
||||
<Block :colCount="3">
|
||||
<!-- No. SEP (input + tombol +) -->
|
||||
<Cell :cosSpan="3">
|
||||
<Label label-for="sep_number">No. SEP</Label>
|
||||
<Field id="sep_number" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="sep_number">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
id="sep_number"
|
||||
v-bind="componentField"
|
||||
placeholder="Tambah SEP terlebih dahulu"
|
||||
class="flex-1"
|
||||
/>
|
||||
<Button class="bg-primary" size="sm" variant="outline" @click.prevent="onAddSep">+</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
|
||||
<!-- Dokumen SEP (file) -->
|
||||
<Cell :cosSpan="3">
|
||||
<Label label-for="sep_file">Dokumen SEP</Label>
|
||||
<Field id="sep_file" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="sep_file">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div class="flex items-center gap-2">
|
||||
<input ref="sepFileInput" type="file" class="hidden" @change="onSepFileChange" />
|
||||
<Button class="bg-primary" size="sm" variant="ghost" @click.prevent="pickSepFile"
|
||||
>Pilih Berkas</Button
|
||||
>
|
||||
<Input readonly v-bind="componentField" placeholder="Unggah dokumen SEP" />
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
|
||||
<!-- Dokumen SIPP (file) -->
|
||||
<Cell :cosSpan="3" labelSize="thin">
|
||||
<Label label-for="sipp_file">Dokumen SIPP</Label>
|
||||
<Field id="sipp_file" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="sipp_file">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div class="flex items-center gap-2">
|
||||
<input ref="sippFileInput" type="file" class="hidden" @change="onSippFileChange" />
|
||||
<Button class="bg-primary" size="sm" variant="ghost" @click.prevent="pickSippFile"
|
||||
>Pilih Berkas</Button
|
||||
>
|
||||
<Input readonly v-bind="componentField" placeholder="Unggah dokumen SIPP" />
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</Cell>
|
||||
</Block>
|
||||
</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"
|
||||
/>
|
||||
<span v-else>+</span>
|
||||
</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>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,94 +7,18 @@ const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dr
|
||||
const statusBadge = defineAsyncComponent(() => import('./status-badge.vue'))
|
||||
|
||||
export const config: Config = {
|
||||
cols: [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{ width: 100 },
|
||||
{ width: 120 },
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{},
|
||||
{ width: 50 },
|
||||
],
|
||||
cols: [{}, {}, {}, {}],
|
||||
|
||||
headers: [
|
||||
[
|
||||
{ label: 'Nama' },
|
||||
{ label: 'Rekam Medis' },
|
||||
{ label: 'KTP' },
|
||||
{ label: 'Tgl Lahir' },
|
||||
{ label: 'Umur' },
|
||||
{ label: 'JK' },
|
||||
{ label: 'Pendidikan' },
|
||||
{ label: 'Status' },
|
||||
{ label: '' },
|
||||
],
|
||||
],
|
||||
headers: [[{ label: 'Kode' }, { label: 'Nama (FHIR)' }, { label: 'Nama (ID)' }, { label: '' }]],
|
||||
|
||||
keys: [
|
||||
'name',
|
||||
'medicalRecord_number',
|
||||
'identity_number',
|
||||
'birth_date',
|
||||
'patient_age',
|
||||
'gender',
|
||||
'education',
|
||||
'status',
|
||||
'action',
|
||||
],
|
||||
keys: ['code', 'name', 'indName', 'action'],
|
||||
|
||||
delKeyNames: [
|
||||
{ key: 'code', label: 'Kode' },
|
||||
{ key: 'name', label: 'Nama' },
|
||||
],
|
||||
|
||||
parses: {
|
||||
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 '-'
|
||||
},
|
||||
},
|
||||
parses: {},
|
||||
|
||||
components: {
|
||||
action(rec, idx) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { config } from './list-cfg'
|
||||
|
||||
defineProps<{ data: any[] }>()
|
||||
const modelValue = defineModel<any | null>()
|
||||
const modelValue = defineModel<any[]>('modelValue', { default: [] })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Trash2 } from 'lucide-vue-next'
|
||||
// import { Button } from '@/components/ui/button'
|
||||
// import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
|
||||
interface Diagnosa {
|
||||
id: number
|
||||
@@ -10,10 +7,10 @@ interface Diagnosa {
|
||||
icd: string
|
||||
}
|
||||
|
||||
const list = ref<Diagnosa[]>([{ id: 1, diagnosa: 'Acute appendicitis', icd: 'K35' }])
|
||||
const modelValue = defineModel<Diagnosa[]>({ default: [] })
|
||||
|
||||
function removeItem(id: number) {
|
||||
list.value = list.value.filter((item) => item.id !== id)
|
||||
modelValue.value = modelValue.value.filter((item) => item.id !== id)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -30,12 +27,19 @@ function removeItem(id: number) {
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
<TableRow v-for="(item, i) in list" :key="item.id">
|
||||
<TableRow
|
||||
v-for="(item, i) in modelValue"
|
||||
:key="item.id"
|
||||
>
|
||||
<TableCell class="text-center font-medium">{{ i + 1 }}</TableCell>
|
||||
<TableCell>{{ item.diagnosa }}</TableCell>
|
||||
<TableCell>{{ item.icd }}</TableCell>
|
||||
<TableCell>{{ item.code }}</TableCell>
|
||||
<TableCell>{{ item.name }}</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Button variant="ghost" size="icon" @click="removeItem(item.id)">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@click="removeItem(item.id)"
|
||||
>
|
||||
<Trash2 class="h-4 w-4 text-gray-500 hover:text-red-500" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
const SelectedRadio = defineAsyncComponent(() => import('~/components/pub/my-ui/data/select-radio.vue'))
|
||||
|
||||
export interface PatientData {
|
||||
id: string
|
||||
identity: string
|
||||
number: string
|
||||
bpjs: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
cols: [{ width: 50 }, { width: 100 }, { width: 100 }, { width: 100 }, { width: 100 }],
|
||||
|
||||
headers: [
|
||||
[{ label: '' }, { label: 'NO. KTP' }, { label: 'NO. RM' }, { label: 'NO. KARTU BPJS' }, { label: 'NAMA PASIEN' }],
|
||||
],
|
||||
|
||||
keys: ['check', 'identity', 'number', 'bpjs', 'name'],
|
||||
|
||||
delKeyNames: [
|
||||
{ key: 'code', label: 'Kode' },
|
||||
{ key: 'name', label: 'Nama' },
|
||||
],
|
||||
|
||||
parses: {},
|
||||
|
||||
components: {
|
||||
check(rec, idx) {
|
||||
return {
|
||||
idx,
|
||||
rec: { ...rec as object, menu: 'patient' },
|
||||
component: SelectedRadio,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
htmls: {},
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||
|
||||
// Types
|
||||
import type { PatientData } from './list-cfg.patient'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
|
||||
// Configs
|
||||
import { config } from './list-cfg.patient'
|
||||
|
||||
const props = defineProps<{
|
||||
data: PatientData[]
|
||||
selected?: string
|
||||
paginationMeta?: PaginationMeta
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
pageChange: [page: number]
|
||||
}>()
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
emit('pageChange', page)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PubMyUiDataTable
|
||||
v-bind="config"
|
||||
:rows="props.data"
|
||||
:selected="props.selected"
|
||||
/>
|
||||
<PaginationView
|
||||
v-if="paginationMeta"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, provide, watch } from 'vue'
|
||||
|
||||
// Components
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from '~/components/pub/ui/dialog'
|
||||
import { Button } from '~/components/pub/ui/button'
|
||||
import { Input } from '~/components/pub/ui/input'
|
||||
import ListPatient from './list-patient.vue'
|
||||
|
||||
// Types
|
||||
import type { PatientData } from './list-cfg.patient'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
|
||||
// Helpers
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
patients: Array<PatientData>
|
||||
selected: string
|
||||
paginationMeta: PaginationMeta
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void
|
||||
(e: 'update:selected', value: string): void
|
||||
(e: 'fetch', value: any): void
|
||||
(e: 'save'): void
|
||||
}>()
|
||||
|
||||
const search = ref('')
|
||||
const debouncedSearch = refDebounced(search, 500) // 500ms debounce
|
||||
|
||||
// Provide for radio selection - use selected prop directly
|
||||
const recSelectId = ref<number>(Number(props.selected) || 0)
|
||||
const recSelectMenu = ref<string>('patient')
|
||||
|
||||
provide('rec_select_id', recSelectId)
|
||||
provide('rec_select_menu', recSelectMenu)
|
||||
|
||||
function saveSelection() {
|
||||
// Validate that a patient is selected
|
||||
if (!props.selected || props.selected === '') {
|
||||
console.warn('No patient selected')
|
||||
return
|
||||
}
|
||||
emit('save')
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
emit('fetch', { 'page-number': page })
|
||||
}
|
||||
|
||||
// Watch for changes in recSelectId and emit update:selected
|
||||
watch(recSelectId, (newValue) => {
|
||||
if (newValue > 0) {
|
||||
emit('update:selected', String(newValue))
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for changes in selected prop
|
||||
watch(() => props.selected, (newValue) => {
|
||||
recSelectId.value = Number(newValue) || 0
|
||||
})
|
||||
|
||||
watch(debouncedSearch, (newValue) => {
|
||||
// Only search if 3+ characters or empty (to clear search)
|
||||
if (newValue.length === 0 || newValue.length >= 3) {
|
||||
emit('fetch', { search: newValue })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:open="props.open"
|
||||
@update:open="emit('update:open', $event)"
|
||||
>
|
||||
<DialogTrigger as-child></DialogTrigger>
|
||||
<DialogContent class="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Cari Pasien</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<!-- Input Search -->
|
||||
<div class="max-w-[50%]">
|
||||
<Input
|
||||
v-model="search"
|
||||
placeholder="Cari berdasarkan No. KTP / No. RM / Nomor Kartu"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border">
|
||||
<ListPatient
|
||||
:data="patients"
|
||||
:selected="props.selected"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="default"
|
||||
class="h-[40px] min-w-[120px] text-white"
|
||||
@click="saveSelection"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-save"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
Simpan
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
const SelectedRadio = defineAsyncComponent(() => import('~/components/pub/my-ui/data/select-radio.vue'))
|
||||
|
||||
export const config: Config = {
|
||||
cols: [
|
||||
{ width: 50 },
|
||||
{ width: 150 },
|
||||
{ width: 120 },
|
||||
{ width: 120 },
|
||||
{ width: 120 },
|
||||
{ width: 120 },
|
||||
{ width: 120 },
|
||||
{ width: 150 },
|
||||
],
|
||||
|
||||
headers: [
|
||||
[
|
||||
{ label: '' },
|
||||
{ label: 'NO. SURAT KONTROL' },
|
||||
{ label: 'TANGGAL SURAT KONTROL' },
|
||||
{ label: 'NO. SEP' },
|
||||
{ label: 'NAMA PASIEN' },
|
||||
{ label: 'NO. KARTU BPJS' },
|
||||
{ label: 'KLINIK TUJUAN' },
|
||||
{ label: 'DOKTER' },
|
||||
],
|
||||
],
|
||||
|
||||
keys: ['check', 'letterNumber', 'plannedDate', 'sepNumber', 'patientName', 'bpjsCardNo', 'clinic', 'doctor'],
|
||||
|
||||
delKeyNames: [
|
||||
{ key: 'code', label: 'Code' },
|
||||
{ key: 'name', label: 'Name' },
|
||||
],
|
||||
|
||||
parses: {},
|
||||
|
||||
components: {
|
||||
check(rec, idx) {
|
||||
return {
|
||||
idx,
|
||||
rec: { ...(rec as object), menu: 'letter' },
|
||||
component: SelectedRadio,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
htmls: {},
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||
|
||||
export interface SepHistoryData {
|
||||
sepNumber: string
|
||||
sepDate: string
|
||||
referralNumber: string
|
||||
diagnosis: string
|
||||
serviceType: string
|
||||
careClass: string
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
cols: [{ width: 100 }, { width: 100 }, { width: 100 }, { width: 100 }, { width: 100 }, { width: 100 }],
|
||||
|
||||
headers: [
|
||||
[
|
||||
{ label: 'NO. SEP' },
|
||||
{ label: 'TGL. SEP' },
|
||||
{ label: 'NO. RUJUKAN' },
|
||||
{ label: 'DIAGNOSIS AWAL' },
|
||||
{ label: 'JENIS PELAYANAN' },
|
||||
{ label: 'KELAS RAWAT' },
|
||||
],
|
||||
],
|
||||
|
||||
keys: ['sepNumber', 'sepDate', 'referralNumber', 'diagnosis', 'serviceType', 'careClass'],
|
||||
|
||||
delKeyNames: [{ key: 'code', label: 'Kode' }],
|
||||
|
||||
parses: {},
|
||||
|
||||
components: {},
|
||||
|
||||
htmls: {},
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
const SelectedRadio = defineAsyncComponent(() => import('~/components/pub/my-ui/data/select-radio.vue'))
|
||||
|
||||
export interface LetterData {
|
||||
letterNumber: string
|
||||
plannedDate: string
|
||||
sepNumber: string
|
||||
patientName: string
|
||||
bpjsCardNo: string
|
||||
clinic: string
|
||||
doctor?: string
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
cols: [{ width: 50 }, { width: 150 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 120 }],
|
||||
|
||||
headers: [
|
||||
[
|
||||
{ label: '' },
|
||||
{ label: 'NO. SURAT RUJUKAN' },
|
||||
{ label: 'TANGGAL SURAT RUJUKAN' },
|
||||
{ label: 'NO. SEP' },
|
||||
{ label: 'NAMA PASIEN' },
|
||||
{ label: 'NO. KARTU BPJS' },
|
||||
{ label: 'KLINIK TUJUAN' },
|
||||
],
|
||||
],
|
||||
|
||||
keys: ['check', 'letterNumber', 'plannedDate', 'sepNumber', 'patientName', 'bpjsCardNo', 'clinic'],
|
||||
|
||||
delKeyNames: [
|
||||
{ key: 'code', label: 'Code' },
|
||||
{ key: 'name', label: 'Name' },
|
||||
],
|
||||
|
||||
parses: {},
|
||||
|
||||
components: {
|
||||
check(rec, idx) {
|
||||
return {
|
||||
idx,
|
||||
rec: { ...(rec as object), menu: 'letter' },
|
||||
component: SelectedRadio,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
htmls: {},
|
||||
}
|
||||
@@ -41,24 +41,24 @@ export const config: Config = {
|
||||
],
|
||||
|
||||
keys: [
|
||||
'tgl_sep',
|
||||
'no_sep',
|
||||
'pelayanan',
|
||||
'jalur',
|
||||
'no_rm',
|
||||
'nama_pasien',
|
||||
'no_kartu_bpjs',
|
||||
'no_surat_kontrol',
|
||||
'tgl_surat_kontrol',
|
||||
'klinik_tujuan',
|
||||
'dpjp',
|
||||
'diagnosis_awal',
|
||||
'letterDate',
|
||||
'letterNumber',
|
||||
'serviceType',
|
||||
'flow',
|
||||
'medicalRecordNumber',
|
||||
'patientName',
|
||||
'cardNumber',
|
||||
'controlLetterNumber',
|
||||
'controlLetterDate',
|
||||
'clinicDestination',
|
||||
'attendingDoctor',
|
||||
'diagnosis',
|
||||
'action',
|
||||
],
|
||||
|
||||
delKeyNames: [
|
||||
{ key: 'no_sep', label: 'NO. SEP' },
|
||||
{ key: 'nama_pasien', label: 'Nama Pasien' },
|
||||
{ key: 'letterNumber', label: 'NO. SEP' },
|
||||
{ key: 'patientName', label: 'Nama Pasien' },
|
||||
],
|
||||
|
||||
parses: {},
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||
|
||||
export interface SepVisitData {
|
||||
letterNumber: string
|
||||
letterDate: string
|
||||
sepNumber: string
|
||||
patientName: string
|
||||
bpjsNumber: string
|
||||
poly: string
|
||||
diagnosis: string
|
||||
serviceType: string
|
||||
careClass: string
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
cols: [
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
],
|
||||
|
||||
headers: [
|
||||
[
|
||||
{ label: 'NO. SURAT KONTROL' },
|
||||
{ label: 'TGL RENCANA KONTROL' },
|
||||
{ label: 'NO. SEP' },
|
||||
{ label: 'NAMA PASIEN' },
|
||||
{ label: 'NO. KARTU BPJS' },
|
||||
{ label: 'DIAGNOSIS AWAL' },
|
||||
{ label: 'JENIS PELAYANAN' },
|
||||
{ label: 'KELAS RAWAT' },
|
||||
],
|
||||
],
|
||||
|
||||
keys: ['letterNumber', 'letterDate', 'sepNumber', 'patientName', 'bpjsNumber', 'diagnosis', 'serviceType', 'careClass'],
|
||||
|
||||
delKeyNames: [{ key: 'code', label: 'Kode' }],
|
||||
|
||||
parses: {},
|
||||
|
||||
components: {},
|
||||
|
||||
htmls: {},
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||
|
||||
// Types
|
||||
import type { SepHistoryData } from './list-cfg.history'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
|
||||
// Configs
|
||||
import { config } from './list-cfg.history'
|
||||
|
||||
const props = defineProps<{
|
||||
data: SepHistoryData[]
|
||||
paginationMeta?: PaginationMeta
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
pageChange: [page: number]
|
||||
}>()
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
emit('pageChange', page)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PubMyUiDataTable
|
||||
v-bind="config"
|
||||
:rows="props.data"
|
||||
/>
|
||||
<PaginationView
|
||||
v-if="paginationMeta"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||
|
||||
// Types
|
||||
import type { LetterData } from './list-cfg.letter'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
|
||||
// Configs
|
||||
import { config as configControl } from './list-cfg.control'
|
||||
import { config as configLetter } from './list-cfg.letter'
|
||||
|
||||
const props = defineProps<{
|
||||
data: LetterData[]
|
||||
menu?: string
|
||||
selected?: string
|
||||
paginationMeta?: PaginationMeta
|
||||
}>()
|
||||
|
||||
const menu = props.menu || 'control'
|
||||
|
||||
const emit = defineEmits<{
|
||||
pageChange: [page: number]
|
||||
}>()
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
emit('pageChange', page)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PubMyUiDataTable
|
||||
v-bind="menu === 'control' ? configControl : configLetter"
|
||||
:rows="props.data"
|
||||
:selected="props.selected"
|
||||
/>
|
||||
<PaginationView
|
||||
v-if="paginationMeta"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||
|
||||
// Types
|
||||
import type { SepVisitData } from './list-cfg.visit'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
|
||||
// Configs
|
||||
import { config } from './list-cfg.history'
|
||||
|
||||
const props = defineProps<{
|
||||
data: SepVisitData[]
|
||||
paginationMeta?: PaginationMeta
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
pageChange: [page: number]
|
||||
}>()
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
emit('pageChange', page)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PubMyUiDataTable
|
||||
v-bind="config"
|
||||
:rows="props.data"
|
||||
/>
|
||||
<PaginationView
|
||||
v-if="paginationMeta"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,23 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { config } from './list-cfg'
|
||||
|
||||
interface SepData {
|
||||
tgl_sep: string
|
||||
no_sep: string
|
||||
pelayanan: string
|
||||
jalur: string
|
||||
no_rm: string
|
||||
nama_pasien: string
|
||||
no_kartu_bpjs: string
|
||||
no_surat_kontrol: string
|
||||
tgl_surat_kontrol: string
|
||||
klinik_tujuan: string
|
||||
dpjp: string
|
||||
diagnosis_awal: string
|
||||
}
|
||||
// Types
|
||||
import type { VclaimSepData } from '~/models/vclaim'
|
||||
|
||||
const props = defineProps<{
|
||||
data: SepData[]
|
||||
data: VclaimSepData[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue"
|
||||
import { Card, CardContent } from "~/components/pub/ui/card"
|
||||
import { Separator } from "~/components/pub/ui/separator"
|
||||
|
||||
// Simulasi data dari API
|
||||
const route = useRoute()
|
||||
const sepData = ref<any>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
// contoh fetch data API (ganti dengan endpoint kamu)
|
||||
const id = route.params.id
|
||||
const res = await fetch(`/api/sep/${id}`)
|
||||
sepData.value = await res.json()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-4xl mx-auto p-6 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-lg font-semibold">Preview SEP</h1>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card class="p-6">
|
||||
<CardContent class="space-y-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<img
|
||||
src="/bpjs-logo.png"
|
||||
alt="BPJS"
|
||||
class="h-10 w-auto"
|
||||
/>
|
||||
<div>
|
||||
<p class="font-semibold">SURAT ELIGIBILITAS PESERTA</p>
|
||||
<p>RSUD dr. Saiful Anwar</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-right">
|
||||
Peserta: {{ sepData?.peserta?.jenisPeserta || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Content -->
|
||||
<div class="grid grid-cols-2 gap-8 text-sm">
|
||||
<!-- Left -->
|
||||
<div class="space-y-1">
|
||||
<p>No. SEP : {{ sepData?.noSEP }}</p>
|
||||
<p>Tgl. SEP : {{ sepData?.tglSEP }}</p>
|
||||
<p>No. Kartu : {{ sepData?.noKartu }}</p>
|
||||
<p>Nama Peserta : {{ sepData?.nama }}</p>
|
||||
<p>Tgl. Lahir : {{ sepData?.tglLahir }} Kelamin: {{ sepData?.kelamin }}</p>
|
||||
<p>No. Telepon : {{ sepData?.telepon }}</p>
|
||||
<p>Sub/Spesialis : {{ sepData?.spesialis }}</p>
|
||||
<p>Dokter : {{ sepData?.dokter }}</p>
|
||||
<p>Faskes Perujuk : {{ sepData?.faskes }}</p>
|
||||
<p>Diagnosa Awal : {{ sepData?.diagnosa }}</p>
|
||||
<p>Catatan : {{ sepData?.catatan }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Right -->
|
||||
<div class="space-y-1">
|
||||
<p>Jns. Rawat : {{ sepData?.jenisRawat }}</p>
|
||||
<p>Jns. Kunjungan : {{ sepData?.jenisKunjungan }}</p>
|
||||
<p>Poli Perujuk : {{ sepData?.poliPerujuk }}</p>
|
||||
<p>Kls. Hak : {{ sepData?.kelasHak }}</p>
|
||||
<p>Kls. Rawat : {{ sepData?.kelasRawat }}</p>
|
||||
<p>Penjamin : {{ sepData?.penjamin }}</p>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<p class="font-semibold">Persetujuan</p>
|
||||
<p>Pasien/Keluarga Pasien</p>
|
||||
<img
|
||||
:src="sepData?.qrCodeUrl"
|
||||
alt="QR Code"
|
||||
class="h-24 mx-auto mt-2"
|
||||
/>
|
||||
<p class="font-semibold mt-2">{{ sepData?.nama }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-xs text-muted-foreground leading-snug space-y-1">
|
||||
<p>*Saya menyetujui BPJS Kesehatan untuk:</p>
|
||||
<ul class="list-disc pl-5">
|
||||
<li>membuka dan atau menggunakan informasi medis Pasien untuk keperluan administrasi dan pembiayaan</li>
|
||||
<li>memberikan akses informasi kepada tenaga medis di RSUD Dr. Saiful Anwar</li>
|
||||
<li>Penjaminan lainnya sesuai ketentuan yang berlaku</li>
|
||||
</ul>
|
||||
<p class="pt-2">
|
||||
Cetakan ke {{ sepData?.cetakanKe || 1 }} |
|
||||
{{ sepData?.tglCetak }}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,218 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import Block from '~/components/pub/my-ui/form/block.vue'
|
||||
import Combobox from '~/components/pub/my-ui/combobox/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'
|
||||
import DatepickerSingle from '~/components/pub/my-ui/datepicker/datepicker-single.vue'
|
||||
|
||||
import { educationCodes, genderCodes, occupationCodes, religionCodes, relationshipCodes } from '~/lib/constants'
|
||||
import { mapToComboboxOptList } from '~/lib/utils'
|
||||
|
||||
interface DivisionFormData {
|
||||
name: string
|
||||
code: string
|
||||
parentId: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
division: {
|
||||
msg: {
|
||||
placeholder: string
|
||||
search: string
|
||||
empty: string
|
||||
}
|
||||
}
|
||||
items: {
|
||||
value: string
|
||||
label: string
|
||||
code: string
|
||||
}[]
|
||||
schema: any
|
||||
initialValues?: Partial<DivisionFormData>
|
||||
errors?: FormErrors
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [values: DivisionFormData, resetForm: () => void]
|
||||
cancel: [resetForm: () => void]
|
||||
}>()
|
||||
|
||||
const relationshipOpts = mapToComboboxOptList(relationshipCodes)
|
||||
const educationOpts = mapToComboboxOptList(educationCodes)
|
||||
const occupationOpts = mapToComboboxOptList(occupationCodes)
|
||||
const genderOpts = mapToComboboxOptList(genderCodes)
|
||||
|
||||
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)
|
||||
}
|
||||
const doctorOpts = ref([
|
||||
{ label: 'Pilih', value: null },
|
||||
{ label: 'Dr. A', value: 1 },
|
||||
])
|
||||
const paymentOpts = ref([
|
||||
{ label: 'Umum', value: 'umum' },
|
||||
{ label: 'BPJS', value: 'bpjs' },
|
||||
])
|
||||
const sepOpts = ref([
|
||||
{ label: 'Rujukan Internal', value: 'ri' },
|
||||
{ label: 'SEP Rujukan', value: 'sr' },
|
||||
])
|
||||
|
||||
// file refs untuk tombol "Pilih Berkas"
|
||||
const sepFileInput = ref<HTMLInputElement | null>(null)
|
||||
const sippFileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
function pickSepFile() {
|
||||
sepFileInput.value?.click()
|
||||
}
|
||||
function pickSippFile() {
|
||||
sippFileInput.value?.click()
|
||||
}
|
||||
|
||||
function onSepFileChange(e: Event) {
|
||||
const f = (e.target as HTMLInputElement).files?.[0]
|
||||
// set ke form / emit / simpan di state sesuai form library-mu
|
||||
console.log('sep file', f)
|
||||
}
|
||||
|
||||
function onSippFileChange(e: Event) {
|
||||
const f = (e.target as HTMLInputElement).files?.[0]
|
||||
console.log('sipp file', f)
|
||||
}
|
||||
|
||||
function onAddSep() {
|
||||
// contoh handler tombol "+" di sebelah No. SEP
|
||||
console.log('open modal tambah SEP')
|
||||
}
|
||||
</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">
|
||||
<div class="p-2">
|
||||
<h2 class="text-md font-semibold">Data Kunjungan</h2>
|
||||
</div>
|
||||
<Block>
|
||||
<!-- Tanggal Daftar (DatePicker) -->
|
||||
<FieldGroup :column="3">
|
||||
<Label label-for="register_date">Dengan Rujukan</Label>
|
||||
<Field id="register_date" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="register_date">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="bpjs_number"
|
||||
v-bind="componentField"
|
||||
placeholder="Pilih jenis pembayaran terlebih dahulu"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
<!-- Jenis Pembayaran (Combobox) -->
|
||||
<FieldGroup :column="3">
|
||||
<Label label-for="payment_type">Rujukan</Label>
|
||||
<Field id="payment_type" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="payment_type">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<!-- <Combobox id="payment_type" v-bind="componentField" :items="paymentOpts" /> -->
|
||||
<Select id="payment_type" v-bind="componentField" :items="paymentOpts" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
<FieldGroup :column="3">
|
||||
<Label label-for="bpjs_number">No. Rujukan</Label>
|
||||
<Field id="bpjs_number" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="bpjs_number">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="bpjs_number"
|
||||
v-bind="componentField"
|
||||
placeholder="Pilih jenis pembayaran terlebih dahulu"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
<!-- No. Kartu BPJS -->
|
||||
<FieldGroup :column="2">
|
||||
<Label label-for="bpjs_number">Tanggal Rujukan</Label>
|
||||
<Field id="bpjs_number" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="bpjs_number">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<DatepickerSingle v-bind="componentField" placeholder="Pilih tanggal" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
<!-- Jenis SEP -->
|
||||
<FieldGroup :column="2">
|
||||
<Label label-for="sep_type">Diagnosis</Label>
|
||||
<Field id="sep_type" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="sep_type">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select id="sep_type" v-bind="componentField" :items="sepOpts" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<!-- No. SEP (input + tombol +) -->
|
||||
|
||||
<FieldGroup>
|
||||
<Label label-for="sep_type">Status Kecelakaan</Label>
|
||||
<Field id="sep_type" :errors="errors">
|
||||
<FormField v-slot="{ componentField }" name="sep_type">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select id="sep_type" v-bind="componentField" :items="sepOpts" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</Block>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -1,81 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from '~/components/pub/ui/dialog'
|
||||
import { Input } from '~/components/pub/ui/input'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
histories: Array<{
|
||||
no_sep: string
|
||||
tgl_sep: string
|
||||
no_rujukan: string
|
||||
diagnosis: string
|
||||
pelayanan: string
|
||||
kelas: string
|
||||
}>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void
|
||||
}>()
|
||||
|
||||
const search = ref('')
|
||||
|
||||
const filteredHistories = computed(() => {
|
||||
const histories = props.histories || []
|
||||
return histories.filter((p) => p.no_sep.includes(search.value))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="props.open" @update:open="emit('update:open', $event)">
|
||||
<DialogTrigger as-child></DialogTrigger>
|
||||
<DialogContent class="max-w-[50%]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>History SEP</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<!-- Input Search -->
|
||||
<div class="mb-2 max-w-[50%]">
|
||||
<Input v-model="search" placeholder="Cari berdasarkan No. SEP" />
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto rounded-lg border">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-100">
|
||||
<tr class="text-left">
|
||||
<th class="p-2">NO. SEP</th>
|
||||
<th class="p-2">TGL. SEP</th>
|
||||
<th class="p-2">NO. RUJUKAN</th>
|
||||
<th class="p-2">DIAGNOSIS AWAL</th>
|
||||
<th class="p-2">JENIS PELAYANAN</th>
|
||||
<th class="p-2">KELAS RAWAT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="font-normal">
|
||||
<tr v-for="p in filteredHistories" :key="p.no_sep" class="border-t hover:bg-gray-50">
|
||||
<td class="p-2">{{ p.no_sep }}</td>
|
||||
<td class="p-2">{{ p.tgl_sep }}</td>
|
||||
<td class="p-2">{{ p.no_rujukan }}</td>
|
||||
<td class="p-2">{{ p.diagnosis }}</td>
|
||||
<td class="p-2">{{ p.pelayanan }}</td>
|
||||
<td class="p-2">{{ p.kelas }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<DialogFooter>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from '~/components/pub/ui/dialog'
|
||||
import { Button } from '~/components/pub/ui/button'
|
||||
import { Input } from '~/components/pub/ui/input'
|
||||
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
letters: Array<{
|
||||
noSurat: string
|
||||
tglRencana: string
|
||||
noSep: string
|
||||
namaPasien: string
|
||||
noBpjs: string
|
||||
klinik: string
|
||||
dokter: string
|
||||
}>
|
||||
selected: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void
|
||||
(e: 'update:selected', value: string): void
|
||||
(e: 'save'): void
|
||||
}>()
|
||||
|
||||
const search = ref('')
|
||||
|
||||
const filteredLetters = computed(() => {
|
||||
const letters = props.letters || []
|
||||
return letters.filter((p) => p.noSurat.includes(search.value) || p.noSep.includes(search.value))
|
||||
})
|
||||
|
||||
function saveSelection() {
|
||||
emit('save')
|
||||
emit('update:open', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="props.open" @update:open="emit('update:open', $event)">
|
||||
<DialogTrigger as-child></DialogTrigger>
|
||||
<DialogContent class="max-w-[50%]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Cari No. Surat Kontrol</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<!-- Input Search -->
|
||||
<div class="mb-2 max-w-[50%]">
|
||||
<Input v-model="search" placeholder="Cari berdasarkan No. Surat Kontrol / No. SEP" />
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto rounded-lg border">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-100">
|
||||
<tr class="text-left">
|
||||
<th class="p-2"></th>
|
||||
<th class="p-2">NO. SURAT KONTROL</th>
|
||||
<th class="p-2">TGL RENCANA KONTROL</th>
|
||||
<th class="p-2">NO. SEP</th>
|
||||
<th class="p-2">NAMA PASIEN</th>
|
||||
<th class="p-2">NO. KARTU BPJS</th>
|
||||
<th class="p-2">KLINIK</th>
|
||||
<th class="p-2">DOKTER</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="font-normal">
|
||||
<tr v-for="p in filteredLetters" :key="p.noSurat" class="border-t hover:bg-gray-50">
|
||||
<td class="p-2">
|
||||
<RadioGroup :model-value="props.selected" @update:model-value="emit('update:selected', $event)">
|
||||
<RadioGroupItem :id="p.noSurat" :value="p.noSurat" />
|
||||
</RadioGroup>
|
||||
</td>
|
||||
<td class="p-2">{{ p.noSurat }}</td>
|
||||
<td class="p-2">{{ p.tglRencana }}</td>
|
||||
<td class="p-2">{{ p.noSep }}</td>
|
||||
<td class="p-2">{{ p.namaPasien }}</td>
|
||||
<td class="p-2">{{ p.noBpjs }}</td>
|
||||
<td class="p-2">{{ p.klinik }}</td>
|
||||
<td class="p-2">{{ p.dokter }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<DialogFooter>
|
||||
<Button variant="default" class="h-[40px] min-w-[120px] text-white" @click="saveSelection">
|
||||
<Icon name="i-lucide-save" class="h-5 w-5" />
|
||||
Simpan
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -1,100 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from '~/components/pub/ui/dialog'
|
||||
import { Button } from '~/components/pub/ui/button'
|
||||
import { Input } from '~/components/pub/ui/input'
|
||||
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
patients: Array<{
|
||||
ktp: string
|
||||
rm: string
|
||||
bpjs: string
|
||||
nama: string
|
||||
}>
|
||||
selected: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void
|
||||
(e: 'update:selected', value: string): void
|
||||
(e: 'save'): void
|
||||
}>()
|
||||
|
||||
const search = ref('')
|
||||
const filteredPatients = computed(() => {
|
||||
const patients = props.patients || []
|
||||
return patients.filter(
|
||||
(p) =>
|
||||
p.ktp.includes(search.value) ||
|
||||
p.rm.includes(search.value) ||
|
||||
p.bpjs.includes(search.value) ||
|
||||
p.nama.toLowerCase().includes(search.value.toLowerCase()),
|
||||
)
|
||||
})
|
||||
|
||||
function saveSelection() {
|
||||
emit('save')
|
||||
emit('update:open', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="props.open" @update:open="emit('update:open', $event)">
|
||||
<DialogTrigger as-child></DialogTrigger>
|
||||
<DialogContent class="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Cari Pasien</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<!-- Input Search -->
|
||||
<div class="mb-2 max-w-[50%]">
|
||||
<Input v-model="search" placeholder="Cari berdasarkan No. KTP / No. RM / Nomor Kartu" />
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto rounded-lg border">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-100">
|
||||
<tr class="text-left">
|
||||
<th class="p-2"></th>
|
||||
<th class="p-2">NO. KTP</th>
|
||||
<th class="p-2">NO. RM</th>
|
||||
<th class="p-2">NO. KARTU BPJS</th>
|
||||
<th class="p-2">NAMA PASIEN</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="font-normal">
|
||||
<tr v-for="p in filteredPatients" :key="p.ktp" class="border-t hover:bg-gray-50">
|
||||
<td class="p-2">
|
||||
<RadioGroup :model-value="props.selected" @update:model-value="emit('update:selected', $event)">
|
||||
<RadioGroupItem :id="p.ktp" :value="p.ktp" />
|
||||
</RadioGroup>
|
||||
</td>
|
||||
<td class="p-2">{{ p.ktp }}</td>
|
||||
<td class="p-2">{{ p.rm }}</td>
|
||||
<td class="p-2">{{ p.bpjs }}</td>
|
||||
<td class="p-2">{{ p.nama }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<DialogFooter>
|
||||
<Button variant="default" class="h-[40px] min-w-[120px] text-white" @click="saveSelection">
|
||||
<Icon name="i-lucide-save" class="h-5 w-5" />
|
||||
Simpan
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from '~/components/pub/ui/dialog'
|
||||
import ListHistory from './list-history.vue'
|
||||
|
||||
// Types
|
||||
import type { SepHistoryData } from './list-cfg.history'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
histories: Array<SepHistoryData>
|
||||
paginationMeta?: PaginationMeta
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:open="props.open"
|
||||
@update:open="emit('update:open', $event)"
|
||||
>
|
||||
<DialogTrigger as-child></DialogTrigger>
|
||||
<DialogContent class="max-w-[50%]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>History SEP</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border">
|
||||
<ListHistory :data="histories" :pagination-meta="paginationMeta" />
|
||||
</div>
|
||||
|
||||
<DialogFooter></DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, provide, watch } from 'vue'
|
||||
|
||||
// Components
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from '~/components/pub/ui/dialog'
|
||||
import { Button } from '~/components/pub/ui/button'
|
||||
import { Input } from '~/components/pub/ui/input'
|
||||
import ListLetter from './list-letter.vue'
|
||||
|
||||
// Types
|
||||
import type { LetterData } from './list-cfg.letter'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
|
||||
// Helpers
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
menu?: string
|
||||
letters: Array<LetterData>
|
||||
selected: string
|
||||
paginationMeta?: PaginationMeta
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void
|
||||
(e: 'update:selected', value: string): void
|
||||
(e: 'fetch', value: any): void
|
||||
(e: 'save'): void
|
||||
}>()
|
||||
|
||||
const search = ref('')
|
||||
const debouncedSearch = refDebounced(search, 500) // 500ms debounce
|
||||
|
||||
// Provide for radio selection - use selected prop directly
|
||||
const recSelectId = ref<string>(props.selected || '')
|
||||
const recSelectMenu = ref<string>('letter')
|
||||
|
||||
provide('rec_select_id', recSelectId)
|
||||
provide('rec_select_menu', recSelectMenu)
|
||||
|
||||
function saveSelection() {
|
||||
// Validate that a letter is selected
|
||||
if (!props.selected || props.selected === '') {
|
||||
console.warn('No letter selected')
|
||||
return
|
||||
}
|
||||
emit('save')
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
emit('fetch', { 'page-number': page })
|
||||
}
|
||||
|
||||
// Watch for changes in recSelectId and emit update:selected
|
||||
watch(recSelectId, (newValue) => {
|
||||
if (newValue && newValue !== '') {
|
||||
emit('update:selected', newValue)
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for changes in selected prop
|
||||
watch(() => props.selected, (newValue) => {
|
||||
recSelectId.value = newValue || ''
|
||||
})
|
||||
|
||||
watch(debouncedSearch, (newValue) => {
|
||||
// Only search if 3+ characters or empty (to clear search)
|
||||
if (newValue.length === 0 || newValue.length >= 3) {
|
||||
emit('fetch', { search: newValue })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:open="props.open"
|
||||
@update:open="emit('update:open', $event)"
|
||||
>
|
||||
<DialogTrigger as-child></DialogTrigger>
|
||||
<DialogContent class="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Search Control Letter</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<!-- Input Search -->
|
||||
<div class="max-w-[50%]">
|
||||
<Input
|
||||
v-model="search"
|
||||
placeholder="Search by Control Letter No. / SEP No."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border">
|
||||
<ListLetter
|
||||
:data="letters"
|
||||
:menu="props.menu"
|
||||
:selected="props.selected"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="default"
|
||||
class="h-[40px] min-w-[120px] text-white"
|
||||
@click="saveSelection"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-save"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -63,13 +63,13 @@ const validate = async () => {
|
||||
}
|
||||
|
||||
defineExpose({ validate })
|
||||
const icdPreview = inject('icdPreview')
|
||||
|
||||
const isExcluded = (key: string) => props.excludeFields?.includes(key)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form id="entry-form">
|
||||
{{ errors }}
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||
<Block>
|
||||
<Cell>
|
||||
@@ -285,7 +285,7 @@ const isExcluded = (key: string) => props.excludeFields?.includes(key)
|
||||
<Button
|
||||
class="rounded bg-orange-100 px-3 py-1 text-orange-600"
|
||||
type="button"
|
||||
@click="emits('modal', 'diagnosa')"
|
||||
@click="emit('modal', 'diagnosa')"
|
||||
>
|
||||
+ Pilih Diagnosa
|
||||
</Button>
|
||||
@@ -298,7 +298,7 @@ const isExcluded = (key: string) => props.excludeFields?.includes(key)
|
||||
<Button
|
||||
class="rounded bg-orange-100 px-3 py-1 text-orange-600"
|
||||
type="button"
|
||||
@click="emits('modal', 'prosedur')"
|
||||
@click="emit('modal', 'prosedur')"
|
||||
>
|
||||
+ Pilih Prosedur
|
||||
</Button>
|
||||
@@ -307,8 +307,8 @@ const isExcluded = (key: string) => props.excludeFields?.includes(key)
|
||||
</Block>
|
||||
|
||||
<div class="mb-8 grid grid-cols-2 gap-4">
|
||||
<AppIcdPreview />
|
||||
<AppIcdPreview />
|
||||
<AppIcdPreview v-model="icdPreview.diagnoses" />
|
||||
<AppIcdPreview v-model="icdPreview.procedures" />
|
||||
</div>
|
||||
|
||||
<Block :colCount="3">
|
||||
|
||||
@@ -85,6 +85,7 @@ const validate = async () => {
|
||||
}
|
||||
|
||||
defineExpose({ validate })
|
||||
const icdPreview = inject('icdPreview')
|
||||
|
||||
const isExcluded = (key: string) => props.excludeFields?.includes(key)
|
||||
</script>
|
||||
@@ -452,10 +453,18 @@ const isExcluded = (key: string) => props.excludeFields?.includes(key)
|
||||
|
||||
<div class="my-2">
|
||||
<h1 class="font-semibold">Diagnosa Fungsional (ICD-X)</h1>
|
||||
|
||||
<Button
|
||||
class="rounded bg-orange-100 px-3 py-1 text-orange-600"
|
||||
type="button"
|
||||
@click="emit('click', 'fungsional')"
|
||||
>
|
||||
+ Pilih Prosedur
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="mb-8 grid grid-cols-2 gap-4">
|
||||
<AppIcdPreview />
|
||||
<AppIcdPreview v-model="icdPreview.diagnoses" />
|
||||
</div>
|
||||
|
||||
<div class="my-2">
|
||||
|
||||
@@ -43,5 +43,6 @@ defineExpose({ validate })
|
||||
@click="$emit('click', $event)"
|
||||
@submit="$emit('submit', $event)"
|
||||
@cancel="$emit('cancel', $event)"
|
||||
@modal="$emit('modal', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -86,6 +86,8 @@ const validate = async () => {
|
||||
|
||||
defineExpose({ validate })
|
||||
|
||||
const icdPreview = inject('icdPreview')
|
||||
|
||||
const isExcluded = (key: string) => props.excludeFields?.includes(key)
|
||||
const disorders = ref<string[]>([])
|
||||
const therapies = ref<string[]>([])
|
||||
@@ -558,33 +560,33 @@ const therapyOptions = ['Terapi Latihan', 'Modalitas Fisik', 'Protesa/Ortosa', '
|
||||
<Button
|
||||
class="my-2 rounded bg-orange-100 px-3 py-1 text-orange-600"
|
||||
type="button"
|
||||
@click="emits('click', 'prosedur')"
|
||||
@click="emit('click', 'diagnosa')"
|
||||
>
|
||||
+ Pilih Prosedur
|
||||
</Button>
|
||||
<AppIcdPreview />
|
||||
<AppIcdPreview v-model="icdPreview.diagnoses" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-md">Diagnosa Fungsional (ICD-X)</span>
|
||||
<Button
|
||||
class="my-2 rounded bg-orange-100 px-3 py-1 text-orange-600"
|
||||
type="button"
|
||||
@click="emit('click', 'fungsional')"
|
||||
>
|
||||
+ Pilih Prosedur
|
||||
</Button>
|
||||
<AppIcdPreview v-model="icdPreview.fungsional" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-md">Diagnosa Medis (ICD-X)</span>
|
||||
<Button
|
||||
class="my-2 rounded bg-orange-100 px-3 py-1 text-orange-600"
|
||||
type="button"
|
||||
@click="emits('click', 'prosedur')"
|
||||
@click="emit('click', 'prosedur')"
|
||||
>
|
||||
+ Pilih Prosedur
|
||||
</Button>
|
||||
<AppIcdPreview />
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-md">Diagnosa Medis (ICD-X)</span>
|
||||
<Button
|
||||
class="my-2 rounded bg-orange-100 px-3 py-1 text-orange-600"
|
||||
type="button"
|
||||
@click="emits('click', 'prosedur')"
|
||||
>
|
||||
+ Pilih Prosedur
|
||||
</Button>
|
||||
<AppIcdPreview />
|
||||
<AppIcdPreview v-model="icdPreview.procedures" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,46 +6,21 @@ type SmallDetailDto = any
|
||||
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
|
||||
|
||||
export const config: Config = {
|
||||
cols: [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{ width: 100 },
|
||||
{ width: 120 },
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{},
|
||||
{ width: 50 },
|
||||
],
|
||||
cols: [{}, {}, {}, { width: 100 }, { width: 120 }, {}, {}, {}, { width: 100 }, { width: 100 }, {}, { width: 50 }],
|
||||
|
||||
headers: [
|
||||
[
|
||||
{ label: 'Nama' },
|
||||
{ label: 'Rekam Medis' },
|
||||
{ label: 'KTP' },
|
||||
{ label: 'Tgl Lahir' },
|
||||
{ label: 'Umur' },
|
||||
{ label: 'JK' },
|
||||
{ label: 'Pendidikan' },
|
||||
{ label: 'Tanggal' },
|
||||
{ label: 'DPJP' },
|
||||
{ label: 'Keluhan & Riwayat' },
|
||||
{ label: 'Pemeriksaan' },
|
||||
{ label: 'Diagnosa' },
|
||||
{ label: 'Status' },
|
||||
{ label: '' },
|
||||
{ label: 'Aksi' },
|
||||
],
|
||||
],
|
||||
|
||||
keys: [
|
||||
'name',
|
||||
'medicalRecord_number',
|
||||
'identity_number',
|
||||
'birth_date',
|
||||
'patient_age',
|
||||
'gender',
|
||||
'education',
|
||||
'status',
|
||||
'action',
|
||||
],
|
||||
keys: ['time', 'employee_id', 'main_complaint', 'encounter_id', 'diagnose', 'status', 'action'],
|
||||
|
||||
delKeyNames: [
|
||||
{ key: 'code', label: 'Kode' },
|
||||
@@ -53,45 +28,34 @@ export const config: Config = {
|
||||
],
|
||||
|
||||
parses: {
|
||||
name: (rec: unknown): unknown => {
|
||||
const recX = rec as SmallDetailDto
|
||||
return `${recX.firstName} ${recX.middleName || ''} ${recX.lastName || ''}`
|
||||
time(rec: any) {
|
||||
return rec.time ? new Date(rec.time).toLocaleDateString() : ''
|
||||
},
|
||||
identity_number: (rec: unknown): unknown => {
|
||||
const recX = rec as SmallDetailDto
|
||||
if (recX.identity_number?.substring(0, 5) === 'BLANK') {
|
||||
return '(TANPA NIK)'
|
||||
main_complaint(rec: any) {
|
||||
const { value } = rec ?? {}
|
||||
|
||||
if (typeof value !== 'string') return '-'
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
console.log('parsed', parsed)
|
||||
return parsed?.['prim-compl'] || '-'
|
||||
} catch {
|
||||
return '-'
|
||||
}
|
||||
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.substring(0, 10)
|
||||
diagnose(rec: any) {
|
||||
const { value } = rec ?? {}
|
||||
|
||||
if (typeof value !== 'string') return '-'
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
const diagnose = parsed?.diagnose || []
|
||||
return diagnose.map((d: any) => d.name).join(', ')
|
||||
} catch {
|
||||
return '-'
|
||||
}
|
||||
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 !== 'undefined') {
|
||||
return recX.education_code
|
||||
}
|
||||
return '-'
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div>halo</div>
|
||||
</template>
|
||||
@@ -1,64 +0,0 @@
|
||||
<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>
|
||||
@@ -1,51 +1,775 @@
|
||||
<script setup lang="ts">
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
// 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 AppViewPatient from '~/components/app/patient/view-patient.vue'
|
||||
|
||||
// Types
|
||||
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
|
||||
import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
|
||||
|
||||
// Constants
|
||||
import { paymentTypes, sepRefTypeCodes, participantGroups } from '~/lib/constants.vclaim'
|
||||
|
||||
// Services
|
||||
import {
|
||||
getList as getSpecialistList,
|
||||
getValueTreeItems as getSpecialistTreeItems,
|
||||
} from '~/services/specialist.service'
|
||||
import { getValueLabelList as getDoctorValueLabelList } from '~/services/doctor.service'
|
||||
import { create as createEncounter, getDetail as getEncounterDetail, update as updateEncounter } from '~/services/encounter.service'
|
||||
import { getList as getSepList } from '~/services/vclaim-sep.service'
|
||||
|
||||
// Helpers
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
patients,
|
||||
selectedPatient,
|
||||
selectedPatientObject,
|
||||
paginationMeta,
|
||||
getPatientsList,
|
||||
getPatientCurrent,
|
||||
getPatientByIdentifierSearch,
|
||||
} from '~/handlers/patient.handler'
|
||||
|
||||
// Stores
|
||||
import { useUserStore } from '~/stores/user'
|
||||
|
||||
const props = defineProps<{
|
||||
id: number
|
||||
classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient'
|
||||
subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
|
||||
formType: string
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const data = ref([])
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const openPatient = ref(false)
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
const paymentsList = ref<Array<{ value: string; label: string }>>([])
|
||||
const sepsList = ref<Array<{ value: string; label: string }>>([])
|
||||
const participantGroupsList = ref<Array<{ value: string; label: string }>>([])
|
||||
const specialistsTree = ref<TreeItem[]>([])
|
||||
const specialistsData = ref<any[]>([]) // Store full specialist data with id
|
||||
const doctorsList = ref<Array<{ value: string; label: string }>>([])
|
||||
const recSelectId = ref<number | null>(null)
|
||||
const isSaving = ref(false)
|
||||
const isLoadingDetail = ref(false)
|
||||
const formRef = ref<InstanceType<typeof AppEncounterEntryForm> | null>(null)
|
||||
const encounterData = ref<any>(null)
|
||||
const formObjects = ref<any>({})
|
||||
|
||||
async function getPatientList() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/patient')
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
// SEP validation state
|
||||
const isSepValid = ref(false)
|
||||
const isCheckingSep = ref(false)
|
||||
const sepNumber = ref('')
|
||||
const debouncedSepNumber = refDebounced(sepNumber, 500)
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
// Computed for edit mode
|
||||
const isEditMode = computed(() => props.id > 0)
|
||||
|
||||
// Computed for save button disable state
|
||||
const isSaveDisabled = computed(() => {
|
||||
return !selectedPatient.value || !selectedPatientObject.value || isSaving.value || isLoadingDetail.value
|
||||
})
|
||||
|
||||
function onClick(e: 'search' | 'add') {
|
||||
console.log('click', e)
|
||||
if (e === 'search') {
|
||||
isOpen.value = true
|
||||
} else if (e === 'add') {
|
||||
navigateTo('/client/patient/add')
|
||||
function getListPath(): string {
|
||||
if (props.classCode === 'ambulatory' && props.subClassCode === 'rehab') {
|
||||
return '/rehab/encounter'
|
||||
}
|
||||
if (props.classCode === 'ambulatory' && props.subClassCode === 'reg') {
|
||||
return '/outpatient/encounter'
|
||||
}
|
||||
if (props.classCode === 'emergency') {
|
||||
return '/emergency/encounter'
|
||||
}
|
||||
if (props.classCode === 'inpatient') {
|
||||
return '/inpatient/encounter'
|
||||
}
|
||||
return '/encounter' // fallback
|
||||
}
|
||||
|
||||
function handleSavePatient() {
|
||||
selectedPatientObject.value = null
|
||||
setTimeout(() => {
|
||||
getPatientCurrent(selectedPatient.value)
|
||||
}, 150)
|
||||
}
|
||||
|
||||
function toKebabCase(str: string): string {
|
||||
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
|
||||
}
|
||||
|
||||
function toNavigateSep(values: any) {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (values['subSpecialistCode']) {
|
||||
const isSub = isSubspecialist(values['subSpecialistCode'], specialistsTree.value)
|
||||
if (!isSub) {
|
||||
values['specialistCode'] = values['subSpecialistCode']
|
||||
delete values['subSpecialistCode']
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(values).forEach((field) => {
|
||||
if (values[field]) {
|
||||
queryParams.append(toKebabCase(field), values[field])
|
||||
}
|
||||
})
|
||||
|
||||
navigateTo('/integration/bpjs/sep/add' + `?${queryParams.toString()}`)
|
||||
}
|
||||
|
||||
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: null,
|
||||
unit_id: null,
|
||||
appointment_doctor_id: Number(formValues.doctorId),
|
||||
responsible_doctor_id: Number(formValues.doctorId),
|
||||
paymentType: formValues.paymentType,
|
||||
cardNumber: formValues.cardNumber,
|
||||
refSource_name: '',
|
||||
appointment_id: null,
|
||||
}
|
||||
|
||||
if (employeeId && employeeId > 0) {
|
||||
payload.adm_employee_id = employeeId
|
||||
}
|
||||
|
||||
// Add specialist_id and subspecialist_id if available
|
||||
if (specialist_id) {
|
||||
payload.specialist_id = specialist_id
|
||||
}
|
||||
if (subspecialist_id) {
|
||||
payload.subspecialist_id = subspecialist_id
|
||||
}
|
||||
|
||||
let paymentMethod = 'cash'
|
||||
if (formValues.paymentType === 'jkn' || formValues.paymentType === 'jkmm') {
|
||||
paymentMethod = 'insurance'
|
||||
} else if (formValues.paymentType === 'spm') {
|
||||
paymentMethod = 'cash'
|
||||
} else if (formValues.paymentType === 'pks') {
|
||||
paymentMethod = 'membership'
|
||||
}
|
||||
|
||||
if (paymentMethod === 'insurance') {
|
||||
payload.paymentMethod_code = paymentMethod
|
||||
payload.insuranceCompany_id = null
|
||||
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 - use update if edit mode, create otherwise
|
||||
let result
|
||||
if (isEditMode.value) {
|
||||
result = await updateEncounter(props.id, payload)
|
||||
} else {
|
||||
result = await createEncounter(payload)
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: 'Berhasil',
|
||||
description: isEditMode.value ? 'Kunjungan berhasil diperbarui' : 'Kunjungan berhasil dibuat',
|
||||
variant: 'default',
|
||||
})
|
||||
// Redirect to list page
|
||||
await navigateTo(getListPath())
|
||||
} else {
|
||||
const errorMessage = result.body?.message || (isEditMode.value ? 'Gagal memperbarui kunjungan' : '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 || (isEditMode.value ? 'Gagal memperbarui kunjungan' : 'Gagal membuat kunjungan'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEvent(menu: string, value?: any) {
|
||||
if (menu === 'search') {
|
||||
getPatientsList({ 'page-size': 10, includes: 'person' }).then(() => {
|
||||
openPatient.value = true
|
||||
})
|
||||
} else if (menu === 'add') {
|
||||
navigateTo('/client/patient/add')
|
||||
} else if (menu === 'add-sep') {
|
||||
// If SEP is already valid, don't navigate
|
||||
if (isSepValid.value) {
|
||||
return
|
||||
}
|
||||
recSelectId.value = null
|
||||
toNavigateSep({
|
||||
isService: 'false',
|
||||
sourcePath: route.path,
|
||||
resource: `${props.classCode}-${props.subClassCode}`,
|
||||
...value,
|
||||
})
|
||||
} else if (menu === 'sep-number-changed') {
|
||||
// Update sepNumber when it changes in form (only if different to prevent loop)
|
||||
if (sepNumber.value !== value) {
|
||||
sepNumber.value = value || ''
|
||||
}
|
||||
} else if (menu === 'save') {
|
||||
await handleSaveEncounter(value)
|
||||
} else if (menu === 'cancel') {
|
||||
navigateTo(getListPath())
|
||||
}
|
||||
}
|
||||
|
||||
// Handle save button click
|
||||
function handleSaveClick() {
|
||||
console.log('🔵 handleSaveClick called')
|
||||
console.log('🔵 formRef:', formRef.value)
|
||||
console.log('🔵 isSaveDisabled:', isSaveDisabled.value)
|
||||
console.log('🔵 selectedPatient:', selectedPatient.value)
|
||||
console.log('🔵 selectedPatientObject:', selectedPatientObject.value)
|
||||
console.log('🔵 isSaving:', isSaving.value)
|
||||
console.log('🔵 isLoadingDetail:', isLoadingDetail.value)
|
||||
|
||||
if (formRef.value && typeof formRef.value.submitForm === 'function') {
|
||||
console.log('🔵 Calling formRef.value.submitForm()')
|
||||
formRef.value.submitForm()
|
||||
} else {
|
||||
console.error('❌ formRef.value is null or submitForm is not a function')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate SEP number
|
||||
*/
|
||||
async function validateSepNumber(sepNumberValue: string) {
|
||||
// Reset validation if SEP number is empty
|
||||
if (!sepNumberValue || sepNumberValue.trim() === '') {
|
||||
isSepValid.value = false
|
||||
isCheckingSep.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// Only check if payment type is JKN
|
||||
// We need to check from formObjects
|
||||
const paymentType = formObjects.value?.paymentType
|
||||
if (paymentType !== 'jkn') {
|
||||
isSepValid.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isCheckingSep.value = true
|
||||
const result = await getSepList({ number: sepNumberValue.trim() })
|
||||
|
||||
// Check if SEP is found
|
||||
// If response is not null, SEP is found
|
||||
// If response is null with metaData code "201", SEP is not found
|
||||
if (result.success && result.body?.response !== null) {
|
||||
isSepValid.value = true
|
||||
} else {
|
||||
// SEP not found (response null with metaData code "201")
|
||||
isSepValid.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking SEP:', error)
|
||||
isSepValid.value = false
|
||||
} finally {
|
||||
isCheckingSep.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Watch debounced SEP number to validate
|
||||
watch(debouncedSepNumber, async (newValue) => {
|
||||
await validateSepNumber(newValue)
|
||||
})
|
||||
|
||||
// Watch payment type to reset SEP validation
|
||||
watch(
|
||||
() => formObjects.value?.paymentType,
|
||||
(newValue) => {
|
||||
isSepValid.value = false
|
||||
// If payment type is not JKN, clear SEP number
|
||||
if (newValue !== 'jkn') {
|
||||
sepNumber.value = ''
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async function handleFetchSpecialists() {
|
||||
try {
|
||||
const specialistsResult = await getSpecialistList({ 'page-size': 100, includes: 'subspecialists' })
|
||||
if (specialistsResult.success) {
|
||||
const specialists = specialistsResult.body?.data || []
|
||||
specialistsData.value = specialists // Store full data for mapping
|
||||
specialistsTree.value = getSpecialistTreeItems(specialists)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching specialist-subspecialist tree:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check if a value exists in the specialistsTree
|
||||
* Returns true if it's a leaf node (subspecialist), false if parent node (specialist)
|
||||
*/
|
||||
function isSubspecialist(value: string, items: TreeItem[]): boolean {
|
||||
for (const item of items) {
|
||||
if (item.value === value) {
|
||||
// If this item has children, it's not selected, so skip
|
||||
// If this is the selected item, check if it has children in the tree
|
||||
return false // This means it's a specialist, not a subspecialist
|
||||
}
|
||||
if (item.children) {
|
||||
for (const child of item.children) {
|
||||
if (child.value === value) {
|
||||
// This is a subspecialist (leaf node)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get specialist/subspecialist code from ID
|
||||
* Returns code string or null if not found
|
||||
*/
|
||||
function getSpecialistCodeFromId(id: number | null | undefined): string | null {
|
||||
if (!id) return null
|
||||
|
||||
// First check if encounter has specialist object with code
|
||||
if (encounterData.value?.specialist?.id === id) {
|
||||
return encounterData.value.specialist.code || null
|
||||
}
|
||||
|
||||
// Search in specialistsData
|
||||
for (const specialist of specialistsData.value) {
|
||||
if (specialist.id === id) {
|
||||
return specialist.code || null
|
||||
}
|
||||
// Check subspecialists
|
||||
if (specialist.subspecialists && Array.isArray(specialist.subspecialists)) {
|
||||
for (const subspecialist of specialist.subspecialists) {
|
||||
if (subspecialist.id === id) {
|
||||
return subspecialist.code || null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get subspecialist code from ID
|
||||
* Returns code string or null if not found
|
||||
*/
|
||||
function getSubspecialistCodeFromId(id: number | null | undefined): string | null {
|
||||
if (!id) return null
|
||||
|
||||
// First check if encounter has subspecialist object with code
|
||||
if (encounterData.value?.subspecialist?.id === id) {
|
||||
return encounterData.value.subspecialist.code || null
|
||||
}
|
||||
|
||||
// Search in specialistsData
|
||||
for (const specialist of specialistsData.value) {
|
||||
if (specialist.subspecialists && Array.isArray(specialist.subspecialists)) {
|
||||
for (const subspecialist of specialist.subspecialists) {
|
||||
if (subspecialist.id === id) {
|
||||
return subspecialist.code || null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
// Build filter based on selection type
|
||||
const filterParams: any = { 'page-size': 100, includes: 'employee-Person' }
|
||||
|
||||
if (!subSpecialistId) {
|
||||
const doctors = await getDoctorValueLabelList(filterParams)
|
||||
doctorsList.value = doctors
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the selectd value is a subspecialist or specialist
|
||||
const isSub = isSubspecialist(subSpecialistId, specialistsTree.value)
|
||||
|
||||
if (isSub) {
|
||||
// If selected is subspecialist, filter by subspecialist-id
|
||||
filterParams['subspecialist-id'] = subSpecialistId
|
||||
} else {
|
||||
// If selected is specialist, filter by specialist-id
|
||||
filterParams['specialist-id'] = subSpecialistId
|
||||
}
|
||||
|
||||
const doctors = await getDoctorValueLabelList(filterParams)
|
||||
doctorsList.value = doctors
|
||||
} catch (error) {
|
||||
console.error('Error fetching doctors:', error)
|
||||
doctorsList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function handleFetch(value?: any) {
|
||||
if (value?.subSpecialistId) {
|
||||
// handleFetchDoctors(value.subSpecialistId)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInit() {
|
||||
selectedPatientObject.value = null
|
||||
paymentsList.value = Object.keys(paymentTypes).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: paymentTypes[item],
|
||||
})) as any
|
||||
sepsList.value = Object.keys(sepRefTypeCodes).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: sepRefTypeCodes[item],
|
||||
})) as any
|
||||
participantGroupsList.value = Object.keys(participantGroups).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: participantGroups[item],
|
||||
})) as any
|
||||
// Fetch tree data
|
||||
await handleFetchDoctors()
|
||||
await handleFetchSpecialists()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load encounter detail data for edit mode
|
||||
*/
|
||||
async function loadEncounterDetail() {
|
||||
if (!isEditMode.value || props.id <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isLoadingDetail.value = true
|
||||
// Include patient, person, specialist, and subspecialist in the response
|
||||
const result = await getEncounterDetail(props.id, {
|
||||
includes: 'patient,patient-person,specialist,subspecialist',
|
||||
})
|
||||
if (result.success && result.body?.data) {
|
||||
encounterData.value = result.body.data
|
||||
await mapEncounterToForm(encounterData.value)
|
||||
// Set loading to false after mapping is complete
|
||||
isLoadingDetail.value = false
|
||||
} else {
|
||||
toast({
|
||||
title: 'Gagal',
|
||||
description: 'Gagal memuat data kunjungan',
|
||||
variant: 'destructive',
|
||||
})
|
||||
// Redirect to list page if encounter not found
|
||||
await navigateTo(getListPath())
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error loading encounter detail:', error)
|
||||
toast({
|
||||
title: 'Gagal',
|
||||
description: error?.message || 'Gagal memuat data kunjungan',
|
||||
variant: 'destructive',
|
||||
})
|
||||
// Redirect to list page on error
|
||||
await navigateTo(getListPath())
|
||||
} finally {
|
||||
isLoadingDetail.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map encounter data to form fields
|
||||
*/
|
||||
async function mapEncounterToForm(encounter: any) {
|
||||
if (!encounter) return
|
||||
|
||||
// Set patient data - use data from response if available
|
||||
if (encounter.patient) {
|
||||
selectedPatient.value = String(encounter.patient.id)
|
||||
selectedPatientObject.value = encounter.patient
|
||||
// Only fetch patient if person data is missing
|
||||
if (!encounter.patient.person) {
|
||||
await getPatientCurrent(selectedPatient.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Map form fields
|
||||
const formData: any = {}
|
||||
|
||||
// Patient data (readonly, populated from selected patient)
|
||||
// Use encounter.patient.person which is already in the response
|
||||
if (encounter.patient?.person) {
|
||||
formData.patientName = encounter.patient.person.name || ''
|
||||
formData.nationalIdentity = encounter.patient.person.residentIdentityNumber || ''
|
||||
formData.medicalRecordNumber = encounter.patient.number || ''
|
||||
} else if (selectedPatientObject.value?.person) {
|
||||
// Fallback to selectedPatientObject if encounter.patient.person is not available
|
||||
formData.patientName = selectedPatientObject.value.person.name || ''
|
||||
formData.nationalIdentity = selectedPatientObject.value.person.residentIdentityNumber || ''
|
||||
formData.medicalRecordNumber = selectedPatientObject.value.number || ''
|
||||
}
|
||||
|
||||
// Doctor ID
|
||||
const doctorId = encounter.appointment_doctor_id || encounter.responsible_doctor_id
|
||||
if (doctorId) {
|
||||
formData.doctorId = String(doctorId)
|
||||
}
|
||||
|
||||
// Specialist/Subspecialist - use helper function to get code from ID
|
||||
// Priority: subspecialist_id > specialist_id
|
||||
if (encounter.subspecialist_id) {
|
||||
const subspecialistCode = getSubspecialistCodeFromId(encounter.subspecialist_id)
|
||||
if (subspecialistCode) {
|
||||
formData.subSpecialistId = subspecialistCode
|
||||
}
|
||||
} else if (encounter.specialist_id) {
|
||||
const specialistCode = getSpecialistCodeFromId(encounter.specialist_id)
|
||||
if (specialistCode) {
|
||||
formData.subSpecialistId = specialistCode
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if encounter has specialist/subspecialist object with code
|
||||
if (!formData.subSpecialistId) {
|
||||
if (encounter.subspecialist?.code) {
|
||||
formData.subSpecialistId = encounter.subspecialist.code
|
||||
} else if (encounter.specialist?.code) {
|
||||
formData.subSpecialistId = encounter.specialist.code
|
||||
}
|
||||
}
|
||||
|
||||
// Register date
|
||||
if (encounter.registeredAt) {
|
||||
// Convert ISO date to local date string (YYYY-MM-DD)
|
||||
const date = new Date(encounter.registeredAt)
|
||||
formData.registerDate = date.toISOString().split('T')[0]
|
||||
} else if (encounter.visitDate) {
|
||||
const date = new Date(encounter.visitDate)
|
||||
formData.registerDate = date.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
// Payment data - use fields directly from encounter
|
||||
// Map paymentMethod_code to paymentType
|
||||
if (encounter.paymentMethod_code) {
|
||||
// Map paymentMethod_code to paymentType
|
||||
// 'insurance' typically means JKN/JKMM
|
||||
if (encounter.paymentMethod_code === 'insurance') {
|
||||
formData.paymentType = 'jkn' // Default to JKN for insurance
|
||||
} else {
|
||||
// For other payment methods, use the code directly if it matches
|
||||
// Otherwise default to 'spm'
|
||||
const validPaymentTypes = ['jkn', 'jkmm', 'spm', 'pks']
|
||||
if (validPaymentTypes.includes(encounter.paymentMethod_code)) {
|
||||
formData.paymentType = encounter.paymentMethod_code
|
||||
} else {
|
||||
formData.paymentType = 'spm' // Default to SPM
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If paymentMethod_code is empty or null, default to 'spm'
|
||||
formData.paymentType = 'spm'
|
||||
}
|
||||
|
||||
// Map payment fields directly from encounter
|
||||
formData.cardNumber = encounter.member_number || ''
|
||||
formData.sepNumber = encounter.ref_number || ''
|
||||
|
||||
// Note: patientCategory and sepType might not be available in the response
|
||||
// These fields might need to be set manually or fetched from other sources
|
||||
|
||||
// Set form objects for the form component
|
||||
formObjects.value = formData
|
||||
|
||||
// Update sepNumber for validation
|
||||
if (formData.sepNumber) {
|
||||
sepNumber.value = formData.sepNumber
|
||||
}
|
||||
|
||||
// Fetch doctors based on specialist/subspecialist selection
|
||||
if (formData.subSpecialistId) {
|
||||
await handleFetchDoctors(formData.subSpecialistId)
|
||||
}
|
||||
}
|
||||
|
||||
provide('rec_select_id', recSelectId)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
onMounted(async () => {
|
||||
await handleInit()
|
||||
// Load encounter detail if in edit mode
|
||||
if (isEditMode.value) {
|
||||
await loadEncounterDetail()
|
||||
}
|
||||
})
|
||||
</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">{{ props.formType }}</span> Kunjungan
|
||||
<Icon
|
||||
name="i-lucide-user"
|
||||
class="me-2"
|
||||
/>
|
||||
<span class="font-semibold">{{ props.formType }}</span>
|
||||
Kunjungan
|
||||
</div>
|
||||
<AppEncounterEntryForm @click="onClick" />
|
||||
<AppSepSmallEntry v-if="props.id" />
|
||||
|
||||
<Dialog v-model:open="isOpen" title="Cari Pasien" size="xl" prevent-outside>
|
||||
<AppPatientPicker :data="data" />
|
||||
</Dialog>
|
||||
<AppEncounterEntryForm
|
||||
ref="formRef"
|
||||
:is-loading="isLoadingDetail"
|
||||
:is-sep-valid="isSepValid"
|
||||
:is-checking-sep="isCheckingSep"
|
||||
:payments="paymentsList"
|
||||
:seps="sepsList"
|
||||
:participant-groups="participantGroupsList"
|
||||
:specialists="specialistsTree"
|
||||
:doctor="doctorsList"
|
||||
:patient="selectedPatientObject"
|
||||
:objects="formObjects"
|
||||
@event="handleEvent"
|
||||
@fetch="handleFetch"
|
||||
/>
|
||||
|
||||
<AppViewPatient
|
||||
v-model:open="openPatient"
|
||||
v-model:selected="selectedPatient"
|
||||
:patients="patients"
|
||||
:pagination-meta="paginationMeta"
|
||||
@fetch="
|
||||
(value) => {
|
||||
if (value.search && value.search.length >= 3) {
|
||||
// Use identifier search for specific searches (NIK, RM, etc.)
|
||||
getPatientByIdentifierSearch(value.search)
|
||||
} else {
|
||||
// Use regular search for general searches
|
||||
getPatientsList({ ...value, 'page-size': 10, includes: 'person' })
|
||||
}
|
||||
}
|
||||
"
|
||||
@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="handleSaveClick"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-save"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
Simpan
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
|
||||
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
// Components
|
||||
import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
|
||||
import SummaryCard from '~/components/pub/my-ui/summary-card/summary-card.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import Filter from '~/components/pub/my-ui/nav-header/filter.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
|
||||
// Types
|
||||
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
|
||||
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// Services
|
||||
import { getList as getEncounterList, remove as removeEncounter } from '~/services/encounter.service'
|
||||
|
||||
// UI
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
const props = defineProps<{
|
||||
classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient'
|
||||
subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
|
||||
type: string
|
||||
}>()
|
||||
|
||||
@@ -21,13 +34,27 @@ const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const isFormEntryDialogOpen = ref(false)
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
|
||||
const hreaderPrep: HeaderPrep = {
|
||||
title: 'Kunjungan',
|
||||
icon: 'i-lucide-users',
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
onClick: () => navigateTo('/rehab/encounter/add'),
|
||||
onClick: () => {
|
||||
if (props.classCode === 'ambulatory' && props.subClassCode === 'rehab') {
|
||||
navigateTo('/rehab/encounter/add')
|
||||
}
|
||||
if (props.classCode === 'ambulatory' && props.subClassCode === 'reg') {
|
||||
navigateTo('/outpatient/encounter/add')
|
||||
}
|
||||
if (props.classCode === 'emergency') {
|
||||
navigateTo('/emergency/encounter/add')
|
||||
}
|
||||
if (props.classCode === 'inpatient') {
|
||||
navigateTo('/inpatient/encounter/add')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -47,40 +74,119 @@ const refSearchNav: RefSearchNav = {
|
||||
|
||||
// Loading state management
|
||||
|
||||
async function getPatientList() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/encounter?includes=patient,patient-person')
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
/**
|
||||
* Get base path for encounter routes based on classCode and subClassCode
|
||||
*/
|
||||
function getBasePath(): string {
|
||||
if (props.classCode === 'ambulatory' && props.subClassCode === 'rehab') {
|
||||
return '/rehab/encounter'
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
if (props.classCode === 'ambulatory' && props.subClassCode === 'reg') {
|
||||
return '/outpatient/encounter'
|
||||
}
|
||||
if (props.classCode === 'emergency') {
|
||||
return '/emergency/encounter'
|
||||
}
|
||||
if (props.classCode === 'inpatient') {
|
||||
return '/inpatient/encounter'
|
||||
}
|
||||
return '/encounter' // fallback
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
})
|
||||
async function getPatientList() {
|
||||
isLoading.isTableLoading = true
|
||||
try {
|
||||
const params: any = { includes: 'patient,patient-person' }
|
||||
if (props.classCode) {
|
||||
params['class-code'] = props.classCode
|
||||
}
|
||||
if (props.subClassCode) {
|
||||
params['sub-class-code'] = props.subClassCode
|
||||
}
|
||||
const result = await getEncounterList(params)
|
||||
if (result.success) {
|
||||
data.value = result.body?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching encounter list:', error)
|
||||
} finally {
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// Handle confirmation result
|
||||
async function handleConfirmDelete(record: any, action: string) {
|
||||
if (action === 'delete' && record?.id) {
|
||||
try {
|
||||
const result = await removeEncounter(record.id)
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: 'Berhasil',
|
||||
description: 'Kunjungan berhasil dihapus',
|
||||
variant: 'default',
|
||||
})
|
||||
await getPatientList() // Refresh list
|
||||
} else {
|
||||
const errorMessage = result.body?.message || 'Gagal menghapus kunjungan'
|
||||
toast({
|
||||
title: 'Gagal',
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error deleting encounter:', error)
|
||||
toast({
|
||||
title: 'Gagal',
|
||||
description: error?.message || 'Gagal menghapus kunjungan',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
// Reset state
|
||||
recId.value = 0
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
isRecordConfirmationOpen.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelConfirmation() {
|
||||
// Reset record state when cancelled
|
||||
recId.value = 0
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
isRecordConfirmationOpen.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => recAction.value,
|
||||
() => {
|
||||
console.log('recAction.value', recAction.value)
|
||||
if (recAction.value === ActionEvents.showConfirmDelete) {
|
||||
isRecordConfirmationOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const basePath = getBasePath()
|
||||
|
||||
if (props.type === 'encounter') {
|
||||
if (recAction.value === 'showDetail') {
|
||||
navigateTo(`/rehab/encounter/${recId.value}/detail`)
|
||||
navigateTo(`${basePath}/${recId.value}/detail`)
|
||||
} else if (recAction.value === 'showEdit') {
|
||||
navigateTo(`/rehab/encounter/${recId.value}/edit`)
|
||||
navigateTo(`${basePath}/${recId.value}/edit`)
|
||||
} else if (recAction.value === 'showProcess') {
|
||||
navigateTo(`/rehab/encounter/${recId.value}/process`)
|
||||
navigateTo(`${basePath}/${recId.value}/process`)
|
||||
} else {
|
||||
// handle other actions
|
||||
}
|
||||
} else if (props.type === 'registration') {
|
||||
// Handle registration type if needed
|
||||
if (recAction.value === 'showDetail') {
|
||||
navigateTo(`/rehab/registration/${recId.value}/detail`)
|
||||
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/detail`)
|
||||
} else if (recAction.value === 'showEdit') {
|
||||
navigateTo(`/rehab/registration/${recId.value}/edit`)
|
||||
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/edit`)
|
||||
} else if (recAction.value === 'showProcess') {
|
||||
navigateTo(`/rehab/registration/${recId.value}/process`)
|
||||
navigateTo(`${basePath.replace('/encounter', '/registration')}/${recId.value}/process`)
|
||||
} else {
|
||||
// handle other actions
|
||||
}
|
||||
@@ -92,6 +198,10 @@ provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -101,7 +211,10 @@ provide('table_data_loader', isLoading)
|
||||
/>
|
||||
<Separator class="my-4 xl:my-5" />
|
||||
|
||||
<Filter :ref-search-nav="refSearchNav" />
|
||||
<Filter
|
||||
:prep="hreaderPrep"
|
||||
:ref-search-nav="refSearchNav"
|
||||
/>
|
||||
|
||||
<AppEncounterList :data="data" />
|
||||
|
||||
@@ -111,6 +224,38 @@ provide('table_data_loader', isLoading)
|
||||
size="lg"
|
||||
prevent-outside
|
||||
>
|
||||
<AppEncounterFilter />
|
||||
<AppEncounterFilter
|
||||
:installation="{
|
||||
msg: { placeholder: 'Pilih' },
|
||||
items: [],
|
||||
}"
|
||||
:schema="{}"
|
||||
/>
|
||||
</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?.patient?.person?.name">
|
||||
<strong>Pasien:</strong>
|
||||
{{ record.patient.person.name }}
|
||||
</p>
|
||||
<p v-if="record?.class_code">
|
||||
<strong>Kelas:</strong>
|
||||
{{ record.class_code }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
|
||||
@@ -11,7 +11,7 @@ import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
|
||||
|
||||
// PLASE ORDER BY TAB POSITION
|
||||
import Status from '~/components/content/encounter/status.vue'
|
||||
import AssesmentFunctionList from '~/components/content/assesment-function/list.vue'
|
||||
import AssesmentFunctionList from '~/components/content/soapi/entry.vue'
|
||||
import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
|
||||
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
|
||||
import PrescriptionList from '~/components/content/prescription/list.vue'
|
||||
@@ -38,13 +38,24 @@ const data = dataResBody?.data ?? null
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{ value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } },
|
||||
{ value: 'early-medical-assessment', label: 'Pengkajian Awal Medis', component: EarlyMedicalAssesmentList },
|
||||
{
|
||||
value: 'early-medical-assessment',
|
||||
label: 'Pengkajian Awal Medis',
|
||||
component: EarlyMedicalAssesmentList,
|
||||
props: { encounter: data, type: 'early-medic', label: 'Pengkajian Awal Medis' },
|
||||
},
|
||||
{
|
||||
value: 'rehab-medical-assessment',
|
||||
label: 'Pengkajian Awal Medis Rehabilitasi Medis',
|
||||
component: EarlyMedicalRehabList,
|
||||
props: { encounter: data, type: 'early-rehab', label: 'Pengkajian Awal Medis Rehabilitasi Medis' },
|
||||
},
|
||||
{
|
||||
value: 'function-assessment',
|
||||
label: 'Asesmen Fungsi',
|
||||
component: AssesmentFunctionList,
|
||||
props: { encounter: data, type: 'function', label: 'Asesmen Fungsi' },
|
||||
},
|
||||
{ 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' },
|
||||
|
||||
@@ -1,113 +1,552 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
// Components
|
||||
import AppSepEntryForm from '~/components/app/sep/entry-form.vue'
|
||||
import AppViewPatient from '~/components/app/patient/view-patient.vue'
|
||||
import AppViewHistory from '~/components/app/sep/view-history.vue'
|
||||
import AppViewLetter from '~/components/app/sep/view-letter.vue'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// Types
|
||||
import type { SepHistoryData } from '~/components/app/sep/list-cfg.history'
|
||||
import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
|
||||
|
||||
// Constants
|
||||
import {
|
||||
serviceTypes,
|
||||
serviceAssessments,
|
||||
registerMethods,
|
||||
trafficAccidents,
|
||||
supportCodes,
|
||||
procedureTypes,
|
||||
purposeOfVisits,
|
||||
classLevels,
|
||||
classLevelUpgrades,
|
||||
classPaySources,
|
||||
} from '~/lib/constants.vclaim'
|
||||
|
||||
// Services
|
||||
import {
|
||||
getList as getSpecialistList,
|
||||
getValueTreeItems as getSpecialistTreeItems,
|
||||
} from '~/services/specialist.service'
|
||||
import { getValueLabelList as getProvinceList } from '~/services/vclaim-region-province.service'
|
||||
import { getValueLabelList as getCityList } from '~/services/vclaim-region-city.service'
|
||||
import { getValueLabelList as getDistrictList } from '~/services/vclaim-region-district.service'
|
||||
import { getValueLabelList as getDoctorLabelList } from '~/services/vclaim-doctor.service'
|
||||
import { getValueLabelList as getHealthFacilityLabelList } from '~/services/vclaim-healthcare.service'
|
||||
import { getValueLabelList as getDiagnoseLabelList } from '~/services/vclaim-diagnose.service'
|
||||
import { getList as getMemberList } from '~/services/vclaim-member.service'
|
||||
import { getList as getHospitalLetterList } from '~/services/vclaim-reference-hospital-letter.service'
|
||||
import { getList as getControlLetterList } from '~/services/vclaim-control-letter.service'
|
||||
import { getList as getMonitoringHistoryList } from '~/services/vclaim-monitoring-history.service'
|
||||
import { create as createSep, makeSepData } from '~/services/vclaim-sep.service'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
patients,
|
||||
selectedPatient,
|
||||
selectedPatientObject,
|
||||
paginationMeta,
|
||||
getPatientsList,
|
||||
getPatientCurrent,
|
||||
getPatientByIdentifierSearch,
|
||||
} from '~/handlers/patient.handler'
|
||||
|
||||
const route = useRoute()
|
||||
const openPatient = ref(false)
|
||||
const openLetter = ref(false)
|
||||
const openHistory = ref(false)
|
||||
const selectedPatient = ref('3456512345678880')
|
||||
const selectedLetter = ref('SK22334442')
|
||||
const selectedLetter = ref('')
|
||||
const selectedObjects = ref<any>({})
|
||||
const selectedServiceType = ref<string>('')
|
||||
const selectedAdmissionType = ref<string>('')
|
||||
const histories = ref<Array<SepHistoryData>>([])
|
||||
const letters = ref<Array<any>>([])
|
||||
const doctors = ref<Array<{ value: string | number; label: string }>>([])
|
||||
const diagnoses = ref<Array<{ value: string | number; label: string }>>([])
|
||||
const facilitiesFrom = ref<Array<{ value: string | number; label: string }>>([])
|
||||
const facilitiesTo = ref<Array<{ value: string | number; label: string }>>([])
|
||||
const supportCodesList = ref<Array<{ value: string; label: string }>>([])
|
||||
const serviceTypesList = ref<Array<{ value: string; label: string }>>([])
|
||||
const registerMethodsList = ref<Array<{ value: string; label: string }>>([])
|
||||
const accidentsList = ref<Array<{ value: string; label: string }>>([])
|
||||
const purposeOfVisitsList = ref<Array<{ value: string; label: string }>>([])
|
||||
const proceduresList = ref<Array<{ value: string; label: string }>>([])
|
||||
const assessmentsList = ref<Array<{ value: string; label: string }>>([])
|
||||
const provincesList = ref<Array<{ value: string; label: string }>>([])
|
||||
const citiesList = ref<Array<{ value: string; label: string }>>([])
|
||||
const districtsList = ref<Array<{ value: string; label: string }>>([])
|
||||
const classLevelsList = ref<Array<{ value: string; label: string }>>([])
|
||||
const classLevelUpgradesList = ref<Array<{ value: string; label: string }>>([])
|
||||
const classPaySourcesList = ref<Array<{ value: string; label: string }>>([])
|
||||
const isServiceHidden = ref(false)
|
||||
const isSaveLoading = ref(false)
|
||||
const isLetterReadonly = ref(false)
|
||||
const specialistsTree = ref<TreeItem[]>([])
|
||||
const resourceType = ref('')
|
||||
const resourcePath = ref('')
|
||||
|
||||
const patients = [
|
||||
{
|
||||
ktp: '3456512345678880',
|
||||
rm: 'RM23311224',
|
||||
bpjs: '334423213214',
|
||||
nama: 'Ahmad Baidowi',
|
||||
},
|
||||
{
|
||||
ktp: '345678804565123',
|
||||
rm: 'RM23455667',
|
||||
bpjs: '33442367656',
|
||||
nama: 'Bian Maulana',
|
||||
},
|
||||
]
|
||||
async function getMonitoringHistoryMappers() {
|
||||
histories.value = []
|
||||
const dateFirst = new Date()
|
||||
const dateLast = new Date()
|
||||
dateLast.setMonth(dateFirst.getMonth() - 3)
|
||||
const cardNumber =
|
||||
selectedPatientObject.value?.person?.residentIdentityNumber || selectedPatientObject.value?.number || ''
|
||||
const result = await getMonitoringHistoryList({
|
||||
cardNumber: cardNumber,
|
||||
startDate: dateFirst.toISOString().substring(0, 10),
|
||||
endDate: dateLast.toISOString().substring(0, 10),
|
||||
})
|
||||
if (result && result.success && result.body) {
|
||||
const historiesRaw = result.body?.response?.histori || []
|
||||
if (!historiesRaw) return
|
||||
historiesRaw.forEach((result: any) => {
|
||||
histories.value.push({
|
||||
sepNumber: result.noSep,
|
||||
sepDate: result.tglSep,
|
||||
referralNumber: result.noRujukan,
|
||||
diagnosis:
|
||||
result.diagnosa && typeof result.diagnosa === 'string' && result.diagnosa.length > 20
|
||||
? result.diagnosa.toString().substring(0, 17) + '...'
|
||||
: '-',
|
||||
serviceType: !result.jnsPelayanan ? '-' : result.jnsPelayanan === '1' ? 'Rawat Jalan' : 'Rawat Inap',
|
||||
careClass: result.kelasRawat,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const letters = [
|
||||
{
|
||||
noSurat: 'SK22334442',
|
||||
tglRencana: '12 Agustus 2025',
|
||||
noSep: 'SEP3232332',
|
||||
namaPasien: 'Ahmad Baidowi',
|
||||
noBpjs: '33442331214',
|
||||
klinik: 'Penyakit Dalam',
|
||||
dokter: 'dr. Andi Prasetyo, Sp.PD-KHOM',
|
||||
},
|
||||
{
|
||||
noSurat: 'SK99120039',
|
||||
tglRencana: '12 Agustus 2025',
|
||||
noSep: 'SEP4443232',
|
||||
namaPasien: 'Bian Maulana',
|
||||
noBpjs: '33442367656',
|
||||
klinik: 'Gigi',
|
||||
dokter: 'dr. Achmad Suparjo',
|
||||
},
|
||||
]
|
||||
async function getLetterMappers(admissionType: string, search: string) {
|
||||
letters.value = []
|
||||
let result = null
|
||||
if (admissionType !== '3') {
|
||||
result = await getHospitalLetterList({
|
||||
letterNumber: search,
|
||||
})
|
||||
} else {
|
||||
result = await getControlLetterList({
|
||||
letterNumber: search,
|
||||
mode: 'by-control',
|
||||
})
|
||||
if (result && result.success && result.body) {
|
||||
const lettersRaw = result.body?.response || null
|
||||
if (!lettersRaw) {
|
||||
result = await getControlLetterList({
|
||||
letterNumber: search,
|
||||
mode: 'by-card',
|
||||
})
|
||||
}
|
||||
}
|
||||
if (result && result.success && result.body) {
|
||||
const lettersRaw = result.body?.response || null
|
||||
if (!lettersRaw) {
|
||||
result = await getControlLetterList({
|
||||
letterNumber: search,
|
||||
mode: 'by-sep',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result && result.success && result.body) {
|
||||
const lettersRaw = result.body?.response || null
|
||||
if (!lettersRaw) return
|
||||
if (admissionType === '3') {
|
||||
letters.value = [
|
||||
{
|
||||
letterNumber: lettersRaw.noSuratKontrol || '',
|
||||
plannedDate: lettersRaw.tglRencanaKontrol || '',
|
||||
sepNumber: lettersRaw.sep.noSep || '',
|
||||
patientName: lettersRaw.sep.peserta.nama || '',
|
||||
bpjsCardNo: lettersRaw.sep.peserta.noKartu,
|
||||
clinic: lettersRaw.sep.poli || '',
|
||||
doctor: lettersRaw.sep.namaDokter || '',
|
||||
},
|
||||
]
|
||||
} else {
|
||||
// integrate ke sep ---
|
||||
// "rujukan": {
|
||||
// "noRujukan": "0212R0300625B000006", // rujukan?.noKunjungan
|
||||
// "ppkRujukan": "0212R030",
|
||||
// "tglRujukan": "2025-06-26",
|
||||
// "asalRujukan": "2" // asalFaskes
|
||||
// },
|
||||
// "jnsPelayanan": "2",
|
||||
// "ppkPelayanan": "1323R001",
|
||||
// "poli": {
|
||||
// "tujuan": "URO", // rujukan?.poliRujukan?.kode
|
||||
// },
|
||||
// "klsRawat": {
|
||||
// "pembiayaan": "",
|
||||
// "klsRawatHak": "2", // peserta.hakKelas?.kode
|
||||
// "klsRawatNaik": "",
|
||||
// "penanggungJawab": ""
|
||||
// },
|
||||
|
||||
const histories = [
|
||||
{
|
||||
no_sep: "SP23311224",
|
||||
tgl_sep: "12 Agustus 2025",
|
||||
no_rujukan: "123444",
|
||||
diagnosis: "C34.9 – Karsinoma Paru",
|
||||
pelayanan: "Rawat Jalan",
|
||||
kelas: "Kelas II",
|
||||
},
|
||||
{
|
||||
no_sep: "SP23455667",
|
||||
tgl_sep: "11 Agustus 2025",
|
||||
no_rujukan: "2331221",
|
||||
diagnosis: "K35 – Apendisitis akut",
|
||||
pelayanan: "Rawat Jalan",
|
||||
kelas: "Kelas II",
|
||||
},
|
||||
]
|
||||
letters.value = [
|
||||
{
|
||||
letterNumber: lettersRaw?.rujukan?.noKunjungan || '',
|
||||
plannedDate: lettersRaw?.rujukan?.tglKunjungan || '',
|
||||
sepNumber: lettersRaw?.rujukan?.informasi?.eSEP || '-',
|
||||
patientName: lettersRaw?.rujukan?.peserta.nama || '',
|
||||
bpjsCardNo: lettersRaw?.rujukan?.peserta.noKartu || '',
|
||||
clinic: lettersRaw?.rujukan?.poliRujukan.nama || '',
|
||||
doctor: '',
|
||||
information: {
|
||||
facility: lettersRaw?.asalFaskes || '',
|
||||
diagnoses: lettersRaw?.rujukan?.diagnosa?.kode || '',
|
||||
serviceType: lettersRaw?.rujukan?.pelayanan?.kode || '',
|
||||
classLevel: lettersRaw?.rujukan?.peserta?.hakKelas?.kode || '',
|
||||
poly: lettersRaw?.rujukan?.poliRujukan?.kode || '',
|
||||
cardNumber: lettersRaw?.rujukan?.peserta?.noKartu || '',
|
||||
patientName: lettersRaw?.rujukan?.peserta?.nama || '',
|
||||
patientPhone: lettersRaw?.rujukan?.peserta?.mr?.noTelepon || '',
|
||||
medicalRecordNumber: lettersRaw?.rujukan?.peserta?.mr?.noMR || '',
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSavePatient() {
|
||||
console.log('Pasien dipilih:', selectedPatient.value)
|
||||
async function getPatientInternalMappers(id: string) {
|
||||
try {
|
||||
await getPatientCurrent(id)
|
||||
if (selectedPatientObject.value) {
|
||||
const patient = selectedPatientObject.value
|
||||
selectedObjects.value['cardNumber'] = '-'
|
||||
selectedObjects.value['nationalIdentity'] = patient?.person?.residentIdentityNumber || '-'
|
||||
selectedObjects.value['medicalRecordNumber'] = patient?.number || '-'
|
||||
selectedObjects.value['patientName'] = patient?.person?.name || '-'
|
||||
selectedObjects.value['phoneNumber'] = patient?.person?.contacts?.[0]?.value || '-'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load patient from query params:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function getPatientExternalMappers(id: string, type: string) {
|
||||
try {
|
||||
const result = await getMemberList({
|
||||
mode: type,
|
||||
number: id,
|
||||
date: new Date().toISOString().substring(0, 10),
|
||||
})
|
||||
if (result && result.success && result.body) {
|
||||
const memberRaws = result.body?.response || null
|
||||
selectedObjects.value['cardNumber'] = memberRaws?.peserta?.noKartu || ''
|
||||
selectedObjects.value['nationalIdentity'] = memberRaws?.peserta?.nik || ''
|
||||
selectedObjects.value['medicalRecordNumber'] = memberRaws?.peserta?.mr?.noMR || ''
|
||||
selectedObjects.value['patientName'] = memberRaws?.peserta?.nama || ''
|
||||
selectedObjects.value['phoneNumber'] = memberRaws?.peserta?.mr?.noTelepon || ''
|
||||
selectedObjects.value['classLevel'] = memberRaws?.peserta?.hakKelas?.kode || ''
|
||||
selectedObjects.value['status'] = memberRaws?.statusPeserta?.kode || ''
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load patient from query params:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaveLetter() {
|
||||
console.log('Letter dipilih:', selectedLetter.value)
|
||||
// Find the selected letter and get its plannedDate
|
||||
const selectedLetterData = letters.value.find((letter) => letter.letterNumber === selectedLetter.value)
|
||||
if (selectedLetterData && selectedLetterData.plannedDate) {
|
||||
selectedObjects.value['letterDate'] = selectedLetterData.plannedDate
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent(value: string) {
|
||||
if (value === 'search-patient') {
|
||||
openPatient.value = true
|
||||
async function handleSavePatient() {
|
||||
selectedPatientObject.value = null
|
||||
await getPatientInternalMappers(selectedPatient.value)
|
||||
}
|
||||
|
||||
async function handleEvent(menu: string, value: any) {
|
||||
if (menu === 'admission-type') {
|
||||
selectedAdmissionType.value = value
|
||||
return
|
||||
}
|
||||
if (value === 'search-letter') {
|
||||
if (menu === 'service-type') {
|
||||
selectedServiceType.value = value
|
||||
doctors.value = await getDoctorLabelList({
|
||||
serviceType: selectedServiceType.value || 1,
|
||||
serviceDate: new Date().toISOString().substring(0, 10),
|
||||
specialistCode: 0,
|
||||
})
|
||||
}
|
||||
if (menu === 'search-patient') {
|
||||
getPatientsList({ 'page-size': 10, includes: 'person' }).then(() => {
|
||||
openPatient.value = true
|
||||
})
|
||||
return
|
||||
}
|
||||
if (menu === 'search-patient-by-identifier') {
|
||||
const text = value.text
|
||||
const type = value.type
|
||||
const prevCardNumber = selectedPatientObject.value?.person?.residentIdentityNumber || ''
|
||||
if (type === 'indentity' && text !== prevCardNumber) {
|
||||
await getPatientByIdentifierSearch(text)
|
||||
await getPatientExternalMappers(text, 'by-identity')
|
||||
}
|
||||
if (type === 'cardNumber' && text !== prevCardNumber) {
|
||||
await getPatientExternalMappers(text, 'by-card')
|
||||
}
|
||||
return
|
||||
}
|
||||
if (menu === 'search-letter') {
|
||||
isLetterReadonly.value = false
|
||||
getLetterMappers(value.admissionType, value.search).then(() => {
|
||||
if (letters.value.length > 0) {
|
||||
const copyObjects = { ...selectedObjects.value }
|
||||
selectedObjects.value = {}
|
||||
selectedLetter.value = letters.value[0].letterNumber
|
||||
isLetterReadonly.value = true
|
||||
setTimeout(() => {
|
||||
selectedObjects.value = copyObjects
|
||||
selectedObjects.value['letterDate'] = letters.value[0].plannedDate
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
if (menu === 'open-letter') {
|
||||
openLetter.value = true
|
||||
return
|
||||
}
|
||||
if (value === 'history-sep') {
|
||||
openHistory.value = true
|
||||
if (menu === 'history-sep') {
|
||||
getMonitoringHistoryMappers().then(() => {
|
||||
openHistory.value = true
|
||||
})
|
||||
return
|
||||
}
|
||||
if (value === 'back') {
|
||||
navigateTo('/bpjs/sep')
|
||||
if (menu === 'back') {
|
||||
navigateTo('/integration/bpjs/sep')
|
||||
}
|
||||
if (menu === 'save-sep') {
|
||||
isSaveLoading.value = true
|
||||
// value.destinationClinic = value.destinationClinic || ''
|
||||
createSep(makeSepData(value))
|
||||
.then((res) => {
|
||||
const body = res?.body
|
||||
const code = body?.metaData?.code
|
||||
const message = body?.metaData?.message
|
||||
if (code && code !== '200') {
|
||||
toast({ title: 'Gagal', description: message || 'Gagal membuat SEP', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
toast({ title: 'Berhasil', description: 'SEP berhasil dibuat', variant: 'default' })
|
||||
if (resourceType.value === 'encounter') {
|
||||
navigateTo(resourcePath.value)
|
||||
return
|
||||
}
|
||||
navigateTo('/integration/bpjs/sep')
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to save SEP:', err)
|
||||
toast({ title: 'Gagal', description: err?.message || 'Gagal membuat SEP', variant: 'destructive' })
|
||||
})
|
||||
.finally(() => {
|
||||
isSaveLoading.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFetch(params: any) {
|
||||
const menu = params.menu || ''
|
||||
const value = params.value || ''
|
||||
if (menu === 'diagnosis') {
|
||||
diagnoses.value = await getDiagnoseLabelList({ diagnosa: value })
|
||||
}
|
||||
if (menu === 'clinic-from') {
|
||||
facilitiesFrom.value = await getHealthFacilityLabelList({
|
||||
healthcare: value,
|
||||
healthcareType: selectedServiceType.value || 1,
|
||||
})
|
||||
}
|
||||
if (menu === 'clinic-to') {
|
||||
facilitiesTo.value = await getHealthFacilityLabelList({
|
||||
healthcare: value,
|
||||
healthcareType: selectedServiceType.value || 1,
|
||||
})
|
||||
}
|
||||
if (menu === 'province') {
|
||||
citiesList.value = await getCityList({ province: value })
|
||||
districtsList.value = []
|
||||
}
|
||||
if (menu === 'city') {
|
||||
districtsList.value = await getDistrictList({ city: value })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFetchSpecialists() {
|
||||
try {
|
||||
const specialistsResult = await getSpecialistList({ 'page-size': 100, includes: 'subspecialists' })
|
||||
if (specialistsResult.success) {
|
||||
const specialists = specialistsResult.body?.data || []
|
||||
specialistsTree.value = getSpecialistTreeItems(specialists)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching specialist-subspecialist tree:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInit() {
|
||||
const facilities = await getHealthFacilityLabelList({
|
||||
healthcare: 'Puskesmas',
|
||||
healthcareType: selectedLetter.value || 1,
|
||||
})
|
||||
diagnoses.value = await getDiagnoseLabelList({ diagnosa: 'paru' })
|
||||
facilitiesFrom.value = facilities
|
||||
facilitiesTo.value = facilities
|
||||
doctors.value = await getDoctorLabelList({
|
||||
serviceType: selectedServiceType.value || 1,
|
||||
serviceDate: new Date().toISOString().substring(0, 10),
|
||||
specialistCode: 0,
|
||||
})
|
||||
provincesList.value = await getProvinceList()
|
||||
serviceTypesList.value = Object.keys(serviceTypes).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: serviceTypes[item],
|
||||
})) as any
|
||||
registerMethodsList.value = Object.keys(registerMethods)
|
||||
.filter((item) => ![''].includes(item))
|
||||
.map((item) => ({
|
||||
value: item.toString(),
|
||||
label: registerMethods[item],
|
||||
})) as any
|
||||
accidentsList.value = Object.keys(trafficAccidents).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: trafficAccidents[item],
|
||||
})) as any
|
||||
purposeOfVisitsList.value = Object.keys(purposeOfVisits).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: purposeOfVisits[item],
|
||||
})) as any
|
||||
proceduresList.value = Object.keys(procedureTypes).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: procedureTypes[item],
|
||||
})) as any
|
||||
assessmentsList.value = Object.keys(serviceAssessments).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: `${item.toString()} - ${serviceAssessments[item]}`,
|
||||
})) as any
|
||||
supportCodesList.value = Object.keys(supportCodes).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: `${item.toString()} - ${supportCodes[item]}`,
|
||||
})) as any
|
||||
classLevelsList.value = Object.keys(classLevels).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: classLevels[item],
|
||||
})) as any
|
||||
classLevelUpgradesList.value = Object.keys(classLevelUpgrades).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: classLevelUpgrades[item],
|
||||
})) as any
|
||||
classPaySourcesList.value = Object.keys(classPaySources).map((item) => ({
|
||||
value: item.toString(),
|
||||
label: classPaySources[item],
|
||||
})) as any
|
||||
await handleFetchSpecialists()
|
||||
if (route.query) {
|
||||
const queries = route.query as any
|
||||
isServiceHidden.value = queries['is-service'] === 'true'
|
||||
selectedObjects.value = {}
|
||||
if (queries['resource']) resourceType.value = queries['resource']
|
||||
if (queries['resource-path']) resourcePath.value = queries['resource-path']
|
||||
if (queries['doctor-code']) selectedObjects.value['doctorCode'] = queries['doctor-code']
|
||||
if (queries['specialist-code']) selectedObjects.value['subSpecialistCode'] = queries['specialist-code']
|
||||
if (queries['sub-specialist-code']) selectedObjects.value['subSpecialistCode'] = queries['sub-specialist-code']
|
||||
if (queries['card-number']) selectedObjects.value['cardNumber'] = queries['card-number']
|
||||
if (queries['register-date']) selectedObjects.value['registerDate'] = queries['register-date']
|
||||
if (queries['sep-type']) selectedObjects.value['sepType'] = queries['sep-type']
|
||||
if (queries['sep-number']) selectedObjects.value['sepNumber'] = queries['sep-number']
|
||||
if (queries['register-date']) selectedObjects.value['registerDate'] = queries['register-date']
|
||||
if (queries['payment-type']) selectedObjects.value['paymentType'] = queries['payment-type']
|
||||
if (queries['patient-id']) {
|
||||
await getPatientInternalMappers(queries['patient-id'])
|
||||
}
|
||||
if (queries['card-number']) {
|
||||
const resultMember = await getMemberList({
|
||||
mode: 'by-card',
|
||||
number: queries['card-number'],
|
||||
date: new Date().toISOString().substring(0, 10),
|
||||
})
|
||||
console.log(resultMember)
|
||||
}
|
||||
delete selectedObjects.value['is-service']
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await handleInit()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||
<Icon name="i-lucide-panel-bottom" class="me-2" />
|
||||
<span class="font-semibold">Tambah</span> SEP
|
||||
<Icon
|
||||
name="i-lucide-panel-bottom"
|
||||
class="me-2"
|
||||
/>
|
||||
<span class="font-semibold">Tambah</span>
|
||||
SEP
|
||||
</div>
|
||||
<AppSepEntryForm @event="handleEvent" />
|
||||
<AppSepTableSearchPatient
|
||||
<AppSepEntryForm
|
||||
:is-save-loading="isSaveLoading"
|
||||
:is-service="isServiceHidden"
|
||||
:doctors="doctors"
|
||||
:diagnoses="diagnoses"
|
||||
:facilities-from="facilitiesFrom"
|
||||
:facilities-to="facilitiesTo"
|
||||
:service-types="serviceTypesList"
|
||||
:register-methods="registerMethodsList"
|
||||
:accidents="accidentsList"
|
||||
:purposes="purposeOfVisitsList"
|
||||
:procedures="proceduresList"
|
||||
:assessments="assessmentsList"
|
||||
:support-codes="supportCodesList"
|
||||
:provinces="provincesList"
|
||||
:cities="citiesList"
|
||||
:districts="districtsList"
|
||||
:class-levels="classLevelsList"
|
||||
:class-level-upgrades="classLevelUpgradesList"
|
||||
:class-pay-sources="classPaySourcesList"
|
||||
:specialists="specialistsTree"
|
||||
:objects="selectedObjects"
|
||||
@fetch="handleFetch"
|
||||
@event="handleEvent"
|
||||
/>
|
||||
<AppViewPatient
|
||||
v-model:open="openPatient"
|
||||
v-model:selected="selectedPatient"
|
||||
:patients="patients"
|
||||
:pagination-meta="paginationMeta"
|
||||
@fetch="
|
||||
(value) => {
|
||||
if (value.search && value.search.length >= 3) {
|
||||
// Use identifier search for specific searches (NIK, RM, etc.)
|
||||
getPatientByIdentifierSearch(value.search)
|
||||
} else {
|
||||
// Use regular search for general searches
|
||||
getPatientsList({ ...value, 'page-size': 10, includes: 'person' })
|
||||
}
|
||||
}
|
||||
"
|
||||
@save="handleSavePatient"
|
||||
/>
|
||||
<AppSepTableSearchLetter
|
||||
v-model:open="openLetter"
|
||||
v-model:selected="selectedLetter"
|
||||
:letters="letters"
|
||||
@save="handleSaveLetter"
|
||||
/>
|
||||
<AppSepTableHistorySep
|
||||
<AppViewHistory
|
||||
v-model:open="openHistory"
|
||||
:histories="histories"
|
||||
/>
|
||||
|
||||
<AppViewLetter
|
||||
v-model:open="openLetter"
|
||||
:letters="letters"
|
||||
:menu="selectedAdmissionType !== '3' ? 'control' : 'reference'"
|
||||
:selected="selectedLetter"
|
||||
:pagination-meta="{ recordCount: 0, page: 1, pageSize: 10, totalPage: 0 } as any"
|
||||
@fetch="(value) => getLetterMappers(value.admissionType, value.search)"
|
||||
@save="handleSaveLetter"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
@@ -9,10 +10,19 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '~/components/pub/ui/dropdown-menu'
|
||||
import AppSepList from '~/components/app/sep/list.vue'
|
||||
|
||||
// Icons
|
||||
import { X, Check } from 'lucide-vue-next'
|
||||
|
||||
// Types
|
||||
import type { VclaimSepData } from '~/models/vclaim'
|
||||
|
||||
// Services
|
||||
import { getList as geMonitoringVisitList } from '~/services/vclaim-monitoring-visit.service'
|
||||
|
||||
const search = ref('')
|
||||
const dateRange = ref('12 Agustus 2025 - 32 Agustus 2025')
|
||||
const dateRange = ref('12 Agustus 2025 - 31 Agustus 2025')
|
||||
const open = ref(false)
|
||||
|
||||
const sepData = {
|
||||
@@ -21,21 +31,6 @@ const sepData = {
|
||||
nama: 'Kenzie',
|
||||
}
|
||||
|
||||
interface SepData {
|
||||
tgl_sep: string
|
||||
no_sep: string
|
||||
pelayanan: string
|
||||
jalur: string
|
||||
no_rm: string
|
||||
nama_pasien: string
|
||||
no_kartu_bpjs: string
|
||||
no_surat_kontrol: string
|
||||
tgl_surat_kontrol: string
|
||||
klinik_tujuan: string
|
||||
dpjp: string
|
||||
diagnosis_awal: string
|
||||
}
|
||||
|
||||
const paginationMeta = reactive<PaginationMeta>({
|
||||
recordCount: 0,
|
||||
page: 1,
|
||||
@@ -49,53 +44,7 @@ function handlePageChange(page: number) {
|
||||
console.log('pageChange', page)
|
||||
}
|
||||
|
||||
const data = ref<SepData[]>([])
|
||||
|
||||
// contoh data dummy
|
||||
const rows = [
|
||||
{
|
||||
tgl_sep: '12 Agustus 2025',
|
||||
no_sep: 'SP23311224',
|
||||
pelayanan: 'Rawat Jalan',
|
||||
jalur: 'Kontrol',
|
||||
no_rm: 'RM23311224',
|
||||
nama_pasien: 'Ahmad Baidowi',
|
||||
no_kartu_bpjs: '334423231214',
|
||||
no_surat_kontrol: 'SK22334442',
|
||||
tgl_surat_kontrol: '13 Agustus 2024',
|
||||
klinik_tujuan: 'Penyakit dalam',
|
||||
dpjp: 'dr. Andi Prasetyo, Sp.PD-KHOM',
|
||||
diagnosis_awal: 'C34.9 - Karsinoma Paru',
|
||||
},
|
||||
{
|
||||
tgl_sep: '12 Agustus 2025',
|
||||
no_sep: 'SP23311224',
|
||||
pelayanan: 'Rawat Jalan',
|
||||
jalur: 'Kontrol',
|
||||
no_rm: 'RM001234',
|
||||
nama_pasien: 'Kenzie',
|
||||
no_kartu_bpjs: '12301234',
|
||||
no_surat_kontrol: '123456',
|
||||
tgl_surat_kontrol: '10 Agustus 2024',
|
||||
klinik_tujuan: 'Penyakit dalam',
|
||||
dpjp: 'Dr. Andreas Sutaji',
|
||||
diagnosis_awal: 'Bronchitis',
|
||||
},
|
||||
{
|
||||
tgl_sep: '11 Agustus 2025',
|
||||
no_sep: 'SP23455667',
|
||||
pelayanan: 'Rawat Jalan',
|
||||
jalur: 'Kontrol',
|
||||
no_rm: 'RM001009',
|
||||
nama_pasien: 'Abraham Sulaiman',
|
||||
no_kartu_bpjs: '334235',
|
||||
no_surat_kontrol: '123334',
|
||||
tgl_surat_kontrol: '11 Agustus 2024',
|
||||
klinik_tujuan: 'Penyakit dalam',
|
||||
dpjp: 'Dr. Andreas Sutaji',
|
||||
diagnosis_awal: 'Paru-paru basah',
|
||||
},
|
||||
]
|
||||
const data = ref<VclaimSepData[]>([])
|
||||
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
@@ -123,18 +72,64 @@ const headerPrep: HeaderPrep = {
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
onClick: () => {
|
||||
navigateTo('/bpjs/sep/add')
|
||||
navigateTo('/integration/bpjs/sep/add')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async function getSepList() {
|
||||
async function getMonitoringVisitMappers() {
|
||||
isLoading.dataListLoading = true
|
||||
data.value = [...rows]
|
||||
data.value = []
|
||||
|
||||
const dateFirst = new Date()
|
||||
const result = await geMonitoringVisitList({
|
||||
date: dateFirst.toISOString().substring(0, 10),
|
||||
serviceType: 1,
|
||||
})
|
||||
|
||||
if (result && result.success && result.body) {
|
||||
const visitsRaw = result.body?.response?.sep || []
|
||||
|
||||
if (!visitsRaw) {
|
||||
isLoading.dataListLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
visitsRaw.forEach((result: any) => {
|
||||
// Format pelayanan: "R.Inap" -> "Rawat Inap", "1" -> "Rawat Jalan", dll
|
||||
let serviceType = result.jnsPelayanan || '-'
|
||||
if (serviceType === 'R.Inap') {
|
||||
serviceType = 'Rawat Inap'
|
||||
} else if (serviceType === '1' || serviceType === 'R.Jalan') {
|
||||
serviceType = 'Rawat Jalan'
|
||||
}
|
||||
|
||||
data.value.push({
|
||||
letterDate: result.tglSep || '-',
|
||||
letterNumber: result.noSep || '-',
|
||||
serviceType: serviceType,
|
||||
flow: '-',
|
||||
medicalRecordNumber: '-',
|
||||
patientName: result.nama || '-',
|
||||
cardNumber: result.noKartu || '-',
|
||||
controlLetterNumber: result.noRujukan || '-',
|
||||
controlLetterDate: result.tglPlgSep || '-',
|
||||
clinicDestination: result.poli || '-',
|
||||
attendingDoctor: '-',
|
||||
diagnosis: result.diagnosa || '-',
|
||||
careClass: result.kelasRawat || '-',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
isLoading.dataListLoading = false
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
async function getSepList() {
|
||||
await getMonitoringVisitMappers()
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
console.log('Ekspor CSV dipilih')
|
||||
// tambahkan logic untuk generate CSV
|
||||
}
|
||||
@@ -169,19 +164,29 @@ provide('table_data_loader', isLoading)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-md border p-4">
|
||||
<div class="p-4">
|
||||
<Header :prep="{ ...headerPrep }" />
|
||||
<!-- Filter Bar -->
|
||||
<div class="my-2 flex flex-wrap items-center gap-2">
|
||||
<!-- Search -->
|
||||
<Input v-model="search" placeholder="Cari No. SEP / No. Kartu BPJS..." class="w-72" />
|
||||
<Input
|
||||
v-model="search"
|
||||
placeholder="Cari No. SEP / No. Kartu BPJS..."
|
||||
class="w-72"
|
||||
/>
|
||||
|
||||
<!-- Date Range -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" class="h-[40px] w-72 border-gray-400 bg-white text-right font-normal">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="h-[40px] w-72 border-gray-400 bg-white text-right font-normal"
|
||||
>
|
||||
{{ dateRange }}
|
||||
<Icon name="i-lucide-calendar" class="h-5 w-5" />
|
||||
<Icon
|
||||
name="i-lucide-calendar"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-2">
|
||||
@@ -196,24 +201,34 @@ provide('table_data_loader', isLoading)
|
||||
variant="outline"
|
||||
class="ml-auto h-[40px] w-[120px] rounded-md border-green-600 text-green-600 hover:bg-green-50"
|
||||
>
|
||||
<Icon name="i-lucide-download" class="h-5 w-5" /> Ekspor
|
||||
<Icon
|
||||
name="i-lucide-download"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
Ekspor
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-40">
|
||||
<DropdownMenuItem @click="exportCsv"> Ekspor CSV </DropdownMenuItem>
|
||||
<DropdownMenuItem @click="exportExcel"> Ekspor Excel </DropdownMenuItem>
|
||||
<DropdownMenuItem @click="exportCsv">Ekspor CSV</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="exportExcel">Ekspor Excel</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border p-4">
|
||||
<AppSepList v-if="!isLoading.dataListLoading" :data="data" />
|
||||
<div class="mt-4">
|
||||
<AppSepList
|
||||
v-if="!isLoading.dataListLoading"
|
||||
:data="data"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template v-if="paginationMeta">
|
||||
<div v-if="paginationMeta.totalPage > 1">
|
||||
<PubMyUiPagination :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||
<PubMyUiPagination
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -226,9 +241,7 @@ provide('table_data_loader', isLoading)
|
||||
<DialogTitle>Hapus SEP</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogDescription class="text-gray-700">
|
||||
Apakah anda yakin ingin menghapus SEP dengan data:
|
||||
</DialogDescription>
|
||||
<DialogDescription class="text-gray-700">Apakah anda yakin ingin menghapus SEP dengan data:</DialogDescription>
|
||||
|
||||
<div class="mt-4 space-y-2 text-sm">
|
||||
<p>No. SEP : {{ sepData.no_sep }}</p>
|
||||
@@ -237,11 +250,21 @@ provide('table_data_loader', isLoading)
|
||||
</div>
|
||||
|
||||
<DialogFooter class="mt-6 flex justify-end gap-3">
|
||||
<Button variant="outline" class="border-green-600 text-green-600 hover:bg-green-50" @click="open = false">
|
||||
<X class="mr-1 h-4 w-4" /> Tidak
|
||||
<Button
|
||||
variant="outline"
|
||||
class="border-green-600 text-green-600 hover:bg-green-50"
|
||||
@click="open = false"
|
||||
>
|
||||
<X class="mr-1 h-4 w-4" />
|
||||
Tidak
|
||||
</Button>
|
||||
<Button variant="destructive" class="bg-red-600 hover:bg-red-700" @click="handleDelete">
|
||||
<Check class="mr-1 h-4 w-4" /> Ya
|
||||
<Button
|
||||
variant="destructive"
|
||||
class="bg-red-600 hover:bg-red-700"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<Check class="mr-1 h-4 w-4" />
|
||||
Ya
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -11,7 +11,7 @@ import FunctionForm from './form-function.vue'
|
||||
const route = useRoute()
|
||||
const type = computed(() => (route.query.tab as string) || 'early-medical-assessment')
|
||||
|
||||
const { mode, openForm, backToList } = useQueryMode('mode')
|
||||
const { mode, goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
|
||||
const formMap = {
|
||||
'early-medical-assessment': EarlyForm,
|
||||
@@ -26,7 +26,8 @@ const ActiveForm = computed(() => formMap[type.value] || EarlyForm)
|
||||
<div>
|
||||
<SoapiList
|
||||
v-if="mode === 'list'"
|
||||
@add="openForm"
|
||||
@add="goToEntry"
|
||||
@edit="goToEntry"
|
||||
/>
|
||||
|
||||
<component
|
||||
|
||||
@@ -2,14 +2,23 @@
|
||||
import { z } from 'zod'
|
||||
import Entry from '~/components/app/soapi/entry.vue'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { FunctionSoapiSchema } from '~/schemas/soapi.schema'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
||||
const { backToList } = useQueryMode('mode')
|
||||
|
||||
const route = useRoute()
|
||||
const isOpen = ref(false)
|
||||
const data = ref([])
|
||||
const isOpenProcedure = ref(false)
|
||||
const isOpenDiagnose = ref(false)
|
||||
const isOpenFungsional = ref(false)
|
||||
const procedures = ref([])
|
||||
const diagnoses = ref([])
|
||||
const fungsional = ref([])
|
||||
const selectedProcedure = ref<any>(null)
|
||||
const selectedDiagnose = ref<any>(null)
|
||||
const selectedFungsional = ref<any>(null)
|
||||
const schema = FunctionSoapiSchema
|
||||
const payload = ref({
|
||||
encounter_id: 0,
|
||||
@@ -60,29 +69,55 @@ const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
|
||||
async function getPatientList() {
|
||||
async function getDiagnoses() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/patient')
|
||||
const resp = await xfetch('/api/v1/diagnose-src')
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
diagnoses.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
async function getProcedures() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/procedure-src')
|
||||
if (resp.success) {
|
||||
procedures.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
getProcedures()
|
||||
getDiagnoses()
|
||||
})
|
||||
|
||||
function handleOpen(type: string) {
|
||||
console.log(type)
|
||||
isOpen.value = true
|
||||
function handleClick(type: string) {
|
||||
if (type === 'prosedur') {
|
||||
isOpenProcedure.value = true
|
||||
} else if (type === 'diagnosa') {
|
||||
isOpenDiagnose.value = true
|
||||
} else if (type === 'fungsional') {
|
||||
isOpenDiagnose.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const entryRehabRef = ref()
|
||||
async function actionHandler(type: string) {
|
||||
console.log(type)
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
return
|
||||
}
|
||||
const result = await entryRehabRef.value?.validate()
|
||||
if (result?.valid) {
|
||||
if (
|
||||
selectedProcedure.value?.length > 0 ||
|
||||
selectedDiagnose.value?.length > 0 ||
|
||||
selectedFungsional.value?.length > 0
|
||||
) {
|
||||
result.data.procedure = selectedProcedure.value || []
|
||||
result.data.diagnose = selectedDiagnose.value || []
|
||||
result.data.fungsional = selectedFungsional.value || []
|
||||
}
|
||||
console.log('data', result.data)
|
||||
handleActionSave(
|
||||
{
|
||||
@@ -99,7 +134,23 @@ async function actionHandler(type: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const icdPreview = ref({
|
||||
procedures: [],
|
||||
diagnoses: [],
|
||||
})
|
||||
|
||||
function actionDialogHandler(type: string) {
|
||||
if (type === 'submit') {
|
||||
icdPreview.value.procedures = selectedProcedure.value || []
|
||||
icdPreview.value.diagnoses = selectedDiagnose.value || []
|
||||
icdPreview.value.fungsional = selectedFungsional.value || []
|
||||
}
|
||||
isOpenProcedure.value = false
|
||||
isOpenDiagnose.value = false
|
||||
}
|
||||
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('icdPreview', icdPreview)
|
||||
</script>
|
||||
<template>
|
||||
<Entry
|
||||
@@ -107,17 +158,52 @@ provide('table_data_loader', isLoading)
|
||||
v-model="model"
|
||||
:schema="schema"
|
||||
type="function"
|
||||
@modal="handleOpen"
|
||||
@click="handleClick"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="actionHandler" />
|
||||
</div>
|
||||
<Dialog
|
||||
v-model:open="isOpen"
|
||||
v-model:open="isOpenProcedure"
|
||||
title="Pilih Prosedur"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker :data="data" />
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedProcedure"
|
||||
:data="procedures"
|
||||
/>
|
||||
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
v-model:open="isOpenDiagnose"
|
||||
title="Pilih Diagnosa"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedDiagnose"
|
||||
:data="diagnoses"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
v-model:open="isOpenFungsional"
|
||||
title="Pilih Fungsional"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedFungsional"
|
||||
:data="diagnoses"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -2,14 +2,20 @@
|
||||
import { z } from 'zod'
|
||||
import Entry from '~/components/app/soapi/entry.vue'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { EarlyRehabSchema } from '~/schemas/soapi.schema'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
||||
const { backToList } = useQueryMode('mode')
|
||||
|
||||
const route = useRoute()
|
||||
const isOpen = ref(false)
|
||||
const data = ref([])
|
||||
const isOpenProcedure = ref(false)
|
||||
const isOpenDiagnose = ref(false)
|
||||
const procedures = ref([])
|
||||
const diagnoses = ref([])
|
||||
const selectedProcedure = ref<any>(null)
|
||||
const selectedDiagnose = ref<any>(null)
|
||||
const schema = EarlyRehabSchema
|
||||
const payload = ref({
|
||||
encounter_id: 0,
|
||||
@@ -65,29 +71,46 @@ const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
|
||||
async function getPatientList() {
|
||||
async function getDiagnoses() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/patient')
|
||||
const resp = await xfetch('/api/v1/diagnose-src')
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
diagnoses.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
async function getProcedures() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/procedure-src')
|
||||
if (resp.success) {
|
||||
procedures.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
getProcedures()
|
||||
getDiagnoses()
|
||||
})
|
||||
|
||||
function handleOpen(type: string) {
|
||||
console.log(type)
|
||||
isOpen.value = true
|
||||
if (type === 'fungsional') {
|
||||
isOpenDiagnose.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const entryRehabRef = ref()
|
||||
async function actionHandler(type: string) {
|
||||
console.log(type)
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
return
|
||||
}
|
||||
const result = await entryRehabRef.value?.validate()
|
||||
if (result?.valid) {
|
||||
if (selectedDiagnose.value?.length > 0) {
|
||||
result.data.diagnose = selectedDiagnose.value || []
|
||||
}
|
||||
console.log('data', result.data)
|
||||
handleActionSave(
|
||||
{
|
||||
@@ -104,7 +127,22 @@ async function actionHandler(type: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const icdPreview = ref({
|
||||
procedures: [],
|
||||
diagnoses: [],
|
||||
})
|
||||
|
||||
function actionDialogHandler(type: string) {
|
||||
if (type === 'submit') {
|
||||
icdPreview.value.procedures = selectedProcedure.value || []
|
||||
icdPreview.value.diagnoses = selectedDiagnose.value || []
|
||||
}
|
||||
isOpenProcedure.value = false
|
||||
isOpenDiagnose.value = false
|
||||
}
|
||||
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('icdPreview', icdPreview)
|
||||
</script>
|
||||
<template>
|
||||
<Entry
|
||||
@@ -112,17 +150,23 @@ provide('table_data_loader', isLoading)
|
||||
v-model="model"
|
||||
:schema="schema"
|
||||
type="early-rehab"
|
||||
@modal="handleOpen"
|
||||
@click="handleOpen"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="actionHandler" />
|
||||
</div>
|
||||
<Dialog
|
||||
v-model:open="isOpen"
|
||||
title="Pilih Prosedur"
|
||||
v-model:open="isOpenDiagnose"
|
||||
title="Pilih Fungsional"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker :data="data" />
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedDiagnose"
|
||||
:data="diagnoses"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -2,14 +2,20 @@
|
||||
import { z } from 'zod'
|
||||
import Entry from '~/components/app/soapi/entry.vue'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { EarlySchema } from '~/schemas/soapi.schema'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
||||
|
||||
const { backToList } = useQueryMode('mode')
|
||||
const route = useRoute()
|
||||
const isOpen = ref(false)
|
||||
const data = ref([])
|
||||
const isOpenProcedure = ref(false)
|
||||
const isOpenDiagnose = ref(false)
|
||||
const procedures = ref([])
|
||||
const diagnoses = ref([])
|
||||
const selectedProcedure = ref<any>(null)
|
||||
const selectedDiagnose = ref<any>(null)
|
||||
const schema = EarlySchema
|
||||
const payload = ref({
|
||||
encounter_id: 0,
|
||||
@@ -38,29 +44,50 @@ const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
|
||||
async function getPatientList() {
|
||||
async function getDiagnoses() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/patient')
|
||||
const resp = await xfetch('/api/v1/diagnose-src')
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
diagnoses.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
async function getProcedures() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/procedure-src')
|
||||
if (resp.success) {
|
||||
procedures.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
getProcedures()
|
||||
getDiagnoses()
|
||||
})
|
||||
|
||||
function handleOpen(type: string) {
|
||||
console.log(type)
|
||||
isOpen.value = true
|
||||
if (type === 'prosedur') {
|
||||
isOpenProcedure.value = true
|
||||
} else if (type === 'diagnosa') {
|
||||
isOpenDiagnose.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const entryRef = ref()
|
||||
async function actionHandler(type: string) {
|
||||
console.log(type)
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
return
|
||||
}
|
||||
const result = await entryRef.value?.validate()
|
||||
if (result?.valid) {
|
||||
if (selectedProcedure.value?.length > 0 || selectedDiagnose.value?.length > 0) {
|
||||
result.data.procedure = selectedProcedure.value || []
|
||||
result.data.diagnose = selectedDiagnose.value || []
|
||||
}
|
||||
|
||||
console.log('data', result.data)
|
||||
handleActionSave(
|
||||
{
|
||||
@@ -77,7 +104,22 @@ async function actionHandler(type: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const icdPreview = ref({
|
||||
procedures: [],
|
||||
diagnoses: [],
|
||||
})
|
||||
|
||||
function actionDialogHandler(type: string) {
|
||||
if (type === 'submit') {
|
||||
icdPreview.value.procedures = selectedProcedure.value || []
|
||||
icdPreview.value.diagnoses = selectedDiagnose.value || []
|
||||
}
|
||||
isOpenProcedure.value = false
|
||||
isOpenDiagnose.value = false
|
||||
}
|
||||
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('icdPreview', icdPreview)
|
||||
</script>
|
||||
<template>
|
||||
<Entry
|
||||
@@ -91,11 +133,32 @@ provide('table_data_loader', isLoading)
|
||||
<Action @click="actionHandler" />
|
||||
</div>
|
||||
<Dialog
|
||||
v-model:open="isOpen"
|
||||
v-model:open="isOpenProcedure"
|
||||
title="Pilih Prosedur"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker :data="data" />
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedProcedure"
|
||||
:data="procedures"
|
||||
/>
|
||||
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
v-model:open="isOpenDiagnose"
|
||||
title="Pilih Diagnosa"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedDiagnose"
|
||||
:data="diagnoses"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -1,16 +1,41 @@
|
||||
<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/soapi/list.vue'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
import { ActionEvents, type HeaderPrep, type RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import List from '~/components/app/soapi/list.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
import { handleActionRemove } from '~/handlers/soapi-early.handler'
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/soapi-early.service'
|
||||
|
||||
// Models
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
encounter: Encounter
|
||||
label: string
|
||||
}
|
||||
const route = useRoute()
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits(['add', 'edit'])
|
||||
|
||||
const data = ref([])
|
||||
const encounterId = ref<number>(props?.encounter?.id || 0)
|
||||
const title = ref('')
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const isLoading = ref(false)
|
||||
const isReadonly = ref(false)
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
const paginationMeta = ref<PaginationMeta>(null)
|
||||
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
@@ -24,13 +49,7 @@ const refSearchNav: RefSearchNav = {
|
||||
},
|
||||
}
|
||||
|
||||
// Loading state management
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const typeCode = ref('')
|
||||
|
||||
const hreaderPrep: HeaderPrep = {
|
||||
title: props.label,
|
||||
@@ -41,15 +60,86 @@ const hreaderPrep: HeaderPrep = {
|
||||
},
|
||||
}
|
||||
|
||||
async function getPatientList() {
|
||||
const resp = await xfetch('/api/v1/patient')
|
||||
const { recordId } = useQueryCRUDRecordId()
|
||||
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
|
||||
const type = computed(() => (route.query.tab as string) || 'early-medical-assessment')
|
||||
|
||||
onMounted(async () => {
|
||||
await getMyList()
|
||||
})
|
||||
|
||||
async function getMyList() {
|
||||
const url = `/api/v1/soapi?type-code=${typeCode.value}?includes=encounter`
|
||||
const resp = await xfetch(url)
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
function handlePageChange(page: number) {
|
||||
emits('pageChange', page)
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
recordId.value = ''
|
||||
backToList()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => type.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
if (val === 'early-medical-assessment') {
|
||||
typeCode.value = 'early-medic'
|
||||
} else if (val === 'rehab-medical-assessment') {
|
||||
typeCode.value = 'early-rehab'
|
||||
} else if (val === 'function-assessment') {
|
||||
typeCode.value = 'function'
|
||||
}
|
||||
getMyList()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showEdit:
|
||||
emits('edit')
|
||||
isReadonly.value = false
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
mode: 'entry',
|
||||
'record-id': recId.value,
|
||||
},
|
||||
})
|
||||
break
|
||||
|
||||
case ActionEvents.showDetail:
|
||||
emits('edit')
|
||||
isReadonly.value = true
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
mode: 'entry',
|
||||
'record-id': recId.value,
|
||||
},
|
||||
})
|
||||
break
|
||||
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
|
||||
case ActionEvents.showAdd:
|
||||
recordId.value = ''
|
||||
goToEntry()
|
||||
emits('add')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
provide('rec_id', recId)
|
||||
@@ -59,7 +149,39 @@ provide('table_data_loader', isLoading)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||
<Header
|
||||
:prep="{ ...hreaderPrep }"
|
||||
:ref-search-nav="refSearchNav"
|
||||
/>
|
||||
<List :data="data" />
|
||||
|
||||
<AssesmentFunctionList :data="data" />
|
||||
<PaginationView
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -16,22 +16,23 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'update:searchText': [value: string]
|
||||
}>()
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
const searchText = ref('')
|
||||
const debouncedSearchText = refDebounced(searchText, 500) // 500ms debounce
|
||||
const selectedItem = computed(() => props.items.find((item) => item.value === props.modelValue))
|
||||
|
||||
const displayText = computed(() => {
|
||||
console.log(selectedItem)
|
||||
if (selectedItem.value?.label) {
|
||||
return selectedItem.value.label
|
||||
}
|
||||
return props.placeholder || 'Pilih item'
|
||||
})
|
||||
|
||||
watch(props, () => {
|
||||
console.log(props.modelValue)
|
||||
watch(debouncedSearchText, (newValue) => {
|
||||
emit('update:searchText', newValue)
|
||||
})
|
||||
|
||||
const searchableItems = computed(() => {
|
||||
@@ -106,6 +107,11 @@ function onSelect(item: Item) {
|
||||
class="h-9 border-0 border-b border-gray-200 bg-white text-black focus:ring-0 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
||||
:placeholder="searchPlaceholder || 'Cari...'"
|
||||
:aria-label="`Cari ${displayText}`"
|
||||
@input="
|
||||
(evt: any) => {
|
||||
searchText = evt?.target?.value || ''
|
||||
}
|
||||
"
|
||||
/>
|
||||
<CommandEmpty class="py-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ emptyMessage || 'Item tidak ditemukan.' }}
|
||||
|
||||
@@ -12,14 +12,14 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
function changeTab(value: string) {
|
||||
activeTab.value = value;
|
||||
emit('changeTab', value);
|
||||
activeTab.value = value
|
||||
emit('changeTab', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Tabs -->
|
||||
<div class="mt-4 flex flex-wrap gap-2 rounded-md border bg-white dark:bg-neutral-950 p-4 shadow-sm">
|
||||
<div class="mt-4 flex flex-wrap gap-2 rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
|
||||
<Button
|
||||
v-for="tab in data"
|
||||
:key="tab.value"
|
||||
@@ -34,10 +34,12 @@ function changeTab(value: string) {
|
||||
<!-- Active Tab Content -->
|
||||
<div class="mt-4 rounded-md border p-4">
|
||||
<component
|
||||
v-if="data.find((t) => t.value === activeTab)?.component"
|
||||
:is="data.find((t) => t.value === activeTab)?.component"
|
||||
:label="data.find((t) => t.value === activeTab)?.label"
|
||||
v-bind="data.find((t) => t.value === activeTab)?.props || {}"
|
||||
v-bind="data.find((t) => t.value === activeTab)?.props"
|
||||
/>
|
||||
<!-- v-if="data.find((t) => t.value === activeTab)?.component" -->
|
||||
<!-- :is="data.find((t) => t.value === activeTab)?.component" -->
|
||||
<!-- v-bind="data.find((t) => t.value === activeTab)?.props || {}" -->
|
||||
<!-- :label="data.find((t) => t.value === activeTab)?.label" -->
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { isRef } from 'vue';
|
||||
import { isRef } from 'vue'
|
||||
import type { DataTableLoader } from '~/components/pub/my-ui/data/types'
|
||||
import type { Config } from './index'
|
||||
import { Info } from 'lucide-vue-next'
|
||||
@@ -19,14 +19,21 @@ const loader = inject('table_data_loader') as DataTableLoader
|
||||
// local state utk selection
|
||||
const selected = ref<any[]>([])
|
||||
|
||||
function toggleSelection(row: any) {
|
||||
if (props.selectMode === 'single') {
|
||||
function toggleSelection(row: any, event?: Event) {
|
||||
if (event) event.stopPropagation() // cegah event bubble ke TableRow
|
||||
|
||||
const isMultiple = props.selectMode === 'multi' || props.selectMode === 'multiple'
|
||||
|
||||
// gunakan pembanding berdasarkan id atau stringify data
|
||||
const findIndex = selected.value.findIndex((r) => JSON.stringify(r) === JSON.stringify(row))
|
||||
|
||||
if (!isMultiple) {
|
||||
// mode single
|
||||
selected.value = [row]
|
||||
emit('update:modelValue', row)
|
||||
} else {
|
||||
const idx = selected.value.findIndex((r) => r === row)
|
||||
if (idx >= 0) {
|
||||
selected.value.splice(idx, 1)
|
||||
if (findIndex >= 0) {
|
||||
selected.value.splice(findIndex, 1)
|
||||
} else {
|
||||
selected.value.push(row)
|
||||
}
|
||||
@@ -35,23 +42,22 @@ function toggleSelection(row: any) {
|
||||
}
|
||||
|
||||
function deepFetch(data: Record<string, any>, key: string): string {
|
||||
let result = '';
|
||||
let result = ''
|
||||
const keys = key.split('.')
|
||||
let lastVal: any = isRef(data) ? {...(data.value as any)} : data
|
||||
let lastVal: any = isRef(data) ? { ...(data.value as any) } : data
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let idx = keys[i] || ''
|
||||
if (typeof lastVal[idx] != undefined && lastVal[idx] != null) {
|
||||
if (i == keys.length - 1) {
|
||||
return lastVal[idx];
|
||||
return lastVal[idx]
|
||||
} else {
|
||||
lastVal = isRef(lastVal[idx]) ? {...(lastVal[idx].value as any)} : lastVal[idx]
|
||||
lastVal = isRef(lastVal[idx]) ? { ...(lastVal[idx].value as any) } : lastVal[idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
function handleActionCellClick(event: Event, _cellRef: string) {
|
||||
// Prevent event if clicked directly on the button/dropdown
|
||||
const target = event.target as HTMLElement
|
||||
@@ -70,7 +76,10 @@ function handleActionCellClick(event: Event, _cellRef: string) {
|
||||
|
||||
<template>
|
||||
<Table>
|
||||
<TableHeader v-if="headers" class="bg-gray-50 dark:bg-gray-800">
|
||||
<TableHeader
|
||||
v-if="headers"
|
||||
class="bg-gray-50 dark:bg-gray-800"
|
||||
>
|
||||
<TableRow v-for="(hr, hrIdx) in headers">
|
||||
<TableHead
|
||||
v-for="(th, idx) in headers[hrIdx]"
|
||||
@@ -85,15 +94,25 @@ function handleActionCellClick(event: Event, _cellRef: string) {
|
||||
|
||||
<TableBody v-if="loader?.isTableLoading">
|
||||
<!-- Loading state with 5 skeleton rows -->
|
||||
<TableRow v-for="n in getSkeletonSize" :key="`skeleton-${n}`">
|
||||
<TableCell v-for="(key, cellIndex) in keys" :key="`cell-skel-${n}-${cellIndex}`" class="border">
|
||||
<Skeleton class="h-6 w-full animate-pulse bg-gray-100 dark:bg-gray-700 text-muted-foreground" />
|
||||
<TableRow
|
||||
v-for="n in getSkeletonSize"
|
||||
:key="`skeleton-${n}`"
|
||||
>
|
||||
<TableCell
|
||||
v-for="(key, cellIndex) in keys"
|
||||
:key="`cell-skel-${n}-${cellIndex}`"
|
||||
class="border"
|
||||
>
|
||||
<Skeleton class="h-6 w-full animate-pulse bg-gray-100 text-muted-foreground dark:bg-gray-700" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
<TableBody v-else-if="rows.length === 0">
|
||||
<TableRow>
|
||||
<TableCell :colspan="keys.length" class="py-8 text-center">
|
||||
<TableCell
|
||||
:colspan="keys.length"
|
||||
class="py-8 text-center"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<Info class="size-5 text-muted-foreground" />
|
||||
<span class="ml-2">Tidak ada data tersedia</span>
|
||||
@@ -106,8 +125,11 @@ function handleActionCellClick(event: Event, _cellRef: string) {
|
||||
v-for="(row, rowIndex) in rows"
|
||||
:key="`row-${rowIndex}`"
|
||||
:class="{
|
||||
'bg-green-50': props.selectMode === 'single' && selected.includes(row),
|
||||
'bg-blue-50': props.selectMode === 'multiple' && selected.includes(row),
|
||||
'bg-green-50':
|
||||
props.selectMode === 'single' && selected.some((r) => JSON.stringify(r) === JSON.stringify(row)),
|
||||
'bg-blue-50':
|
||||
(props.selectMode === 'multi' || props.selectMode === 'multiple') &&
|
||||
selected.some((r) => JSON.stringify(r) === JSON.stringify(row)),
|
||||
}"
|
||||
@click="toggleSelection(row)"
|
||||
>
|
||||
@@ -116,14 +138,23 @@ function handleActionCellClick(event: Event, _cellRef: string) {
|
||||
<input
|
||||
v-if="props.selectMode === 'single'"
|
||||
type="radio"
|
||||
:checked="selected.includes(row)"
|
||||
@change="toggleSelection(row)"
|
||||
:checked="selected.some((r) => JSON.stringify(r) === JSON.stringify(row))"
|
||||
@click.stop="toggleSelection(row, $event)"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
type="checkbox"
|
||||
:checked="selected.some((r) => JSON.stringify(r) === JSON.stringify(row))"
|
||||
@click.stop="toggleSelection(row, $event)"
|
||||
/>
|
||||
<input v-else type="checkbox" :checked="selected.includes(row)" @change="toggleSelection(row)" />
|
||||
</TableCell>
|
||||
|
||||
<!-- lanjut render cell normal -->
|
||||
<TableCell v-for="(key, cellIndex) in keys" :key="`cell-${rowIndex}-${cellIndex}`" class="border">
|
||||
<TableCell
|
||||
v-for="(key, cellIndex) in keys"
|
||||
:key="`cell-${rowIndex}-${cellIndex}`"
|
||||
class="border"
|
||||
>
|
||||
<!-- existing cell renderer -->
|
||||
<component
|
||||
:is="components?.[key]?.(row, rowIndex).component"
|
||||
@@ -133,9 +164,12 @@ function handleActionCellClick(event: Event, _cellRef: string) {
|
||||
v-bind="components[key]?.(row, rowIndex).props"
|
||||
/>
|
||||
<template v-else>
|
||||
<div v-if="htmls?.[key]" v-html="htmls?.[key]?.(row, rowIndex)"></div>
|
||||
<div
|
||||
v-if="htmls?.[key]"
|
||||
v-html="htmls?.[key]?.(row, rowIndex)"
|
||||
></div>
|
||||
<template v-else>
|
||||
{{ parses?.[key]?.(row, rowIndex) ?? deepFetch((row as any), key) }}
|
||||
{{ parses?.[key]?.(row, rowIndex) ?? deepFetch(row as any, key) }}
|
||||
</template>
|
||||
</template>
|
||||
</TableCell>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, type Ref } from 'vue'
|
||||
// Components
|
||||
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
||||
|
||||
const props = defineProps<{
|
||||
rec: { id: string; name: string; menu: string }
|
||||
selected?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
// No emits needed - using provide/inject
|
||||
}>()
|
||||
|
||||
const record = props.rec || {}
|
||||
const recId = inject('rec_select_id') as Ref<number>
|
||||
const recMenu = inject('rec_select_menu') as Ref<string>
|
||||
const selected = computed(() => recId.value === Number(record.id) ? record.id : '')
|
||||
|
||||
function handleSelection(value: string) {
|
||||
if (value === record.id) {
|
||||
recId.value = Number(record.id) || 0
|
||||
recMenu.value = record.menu || ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RadioGroup
|
||||
:model-value="selected"
|
||||
@update:model-value="handleSelection"
|
||||
>
|
||||
<RadioGroupItem
|
||||
:id="record.id"
|
||||
:value="record.id"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
@@ -34,9 +34,18 @@ const df = new DateFormatter('en-US', {
|
||||
dateStyle: 'medium',
|
||||
})
|
||||
|
||||
// Get current date
|
||||
const today = new Date()
|
||||
const todayCalendar = new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate())
|
||||
|
||||
// Get date 1 month ago
|
||||
const oneMonthAgo = new Date(today)
|
||||
oneMonthAgo.setMonth(today.getMonth() - 1)
|
||||
const oneMonthAgoCalendar = new CalendarDate(oneMonthAgo.getFullYear(), oneMonthAgo.getMonth() + 1, oneMonthAgo.getDate())
|
||||
|
||||
const value = ref({
|
||||
start: new CalendarDate(2022, 1, 20),
|
||||
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
|
||||
start: oneMonthAgoCalendar,
|
||||
end: todayCalendar,
|
||||
}) as Ref<DateRange>
|
||||
|
||||
function onFilterClick() {
|
||||
|
||||
@@ -1,8 +1,128 @@
|
||||
// Handlers
|
||||
import { genCrudHandler } from '~/handlers/_handler'
|
||||
|
||||
// Types
|
||||
import type { PatientEntity } from '~/models/patient'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
|
||||
// Services
|
||||
import { postPatient as create, patchPatient as update, removePatient as remove } from '~/services/patient.service'
|
||||
import {
|
||||
postPatient as create,
|
||||
patchPatient as update,
|
||||
removePatient as remove,
|
||||
getPatientDetail,
|
||||
getPatients,
|
||||
getPatientByIdentifier,
|
||||
} from '~/services/patient.service'
|
||||
|
||||
const isPatientsLoading = ref(false)
|
||||
const patients = ref<Array<{ id: string; identity: string; number: string; bpjs: string; name: string }>>([])
|
||||
const selectedPatient = ref<string>('')
|
||||
const selectedPatientObject = ref<PatientEntity | null>(null)
|
||||
const paginationMeta = ref<PaginationMeta>({
|
||||
recordCount: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalPage: 5,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
})
|
||||
|
||||
function mapPatientToRow(patient: PatientEntity) {
|
||||
const identity = patient?.person?.residentIdentityNumber || '-'
|
||||
const number = patient?.number || '-'
|
||||
const bpjs = '-'
|
||||
const name = patient?.person?.name || '-'
|
||||
return { id: patient.id ? String(patient.id) : '-', identity, number, bpjs, name }
|
||||
}
|
||||
|
||||
function mapPaginationMetaToRow(meta: any) {
|
||||
const recordCount = meta['record_totalCount'] ? Number(meta['record_totalCount']) : 0
|
||||
const currentCount = meta['record_currentCount'] ? Number(meta['record_currentCount']) : 0
|
||||
const page = meta['page_number'] ? Number(meta['page_number']) : 1
|
||||
const pageSize = meta['page_size'] ? Number(meta['page_size']) : 10
|
||||
const totalPage = Math.ceil(recordCount / pageSize)
|
||||
|
||||
return {
|
||||
recordCount,
|
||||
page,
|
||||
pageSize,
|
||||
totalPage,
|
||||
hasNext: currentCount < recordCount && page < totalPage,
|
||||
hasPrev: page > 1,
|
||||
}
|
||||
}
|
||||
|
||||
async function getPatientsList(params: any = { 'page-size': 10 }) {
|
||||
try {
|
||||
isPatientsLoading.value = true
|
||||
patients.value = []
|
||||
paginationMeta.value = {} as PaginationMeta
|
||||
const result = await getPatients(params)
|
||||
if (result && result.success && result.body && Array.isArray(result.body.data)) {
|
||||
const meta = result.body.meta
|
||||
patients.value = result.body.data.map(mapPatientToRow)
|
||||
paginationMeta.value = mapPaginationMetaToRow(meta)
|
||||
} else {
|
||||
patients.value = [] // fallback to empty array
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch patients for SEP search:', err)
|
||||
patients.value = []
|
||||
} finally {
|
||||
isPatientsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function getPatientCurrent(id: string) {
|
||||
try {
|
||||
const result = await getPatientDetail(Number(id))
|
||||
if (result && result.success && result.body && result.body.data) {
|
||||
const patient = result.body.data || null
|
||||
selectedPatientObject.value = patient
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch patient:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function getPatientByIdentifierSearch(search: string) {
|
||||
try {
|
||||
isPatientsLoading.value = true
|
||||
patients.value = []
|
||||
paginationMeta.value = {} as PaginationMeta
|
||||
const result = await getPatientByIdentifier(search)
|
||||
if (result && result.success && result.body) {
|
||||
if (result.type === 'resident-identity' && result.body.data) {
|
||||
patients.value = [mapPatientToRow(result.body.data)]
|
||||
} else if (result.type === 'identity') {
|
||||
patients.value = Array.isArray(result.body.data) ? result.body.data.map(mapPatientToRow) : result.body.data ? [mapPatientToRow(result.body.data)] : []
|
||||
} else {
|
||||
const meta = result.body.meta
|
||||
patients.value = Array.isArray(result.body.data) ? result.body.data.map(mapPatientToRow) : result.body.data ? [mapPatientToRow(result.body.data)] : []
|
||||
paginationMeta.value = mapPaginationMetaToRow(meta)
|
||||
}
|
||||
} else {
|
||||
patients.value = [] // fallback to empty array
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch patients by identifier:', err)
|
||||
patients.value = []
|
||||
} finally {
|
||||
isPatientsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
isPatientsLoading,
|
||||
patients,
|
||||
selectedPatient,
|
||||
selectedPatientObject,
|
||||
paginationMeta,
|
||||
getPatientsList,
|
||||
getPatientCurrent,
|
||||
getPatientByIdentifierSearch,
|
||||
}
|
||||
|
||||
export const {
|
||||
recId,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
export const serviceTypes: Record<string, string> = {
|
||||
'1': 'Rawat Inap',
|
||||
'2': 'Rawat Jalan',
|
||||
}
|
||||
|
||||
export const registerMethods: Record<string, string> = {
|
||||
'1': 'Rujukan',
|
||||
'2': 'IGD',
|
||||
'3': 'Kontrol',
|
||||
'4': 'Rujukan Internal',
|
||||
}
|
||||
|
||||
export const classLevels: Record<string, string> = {
|
||||
'1': 'Kelas 1',
|
||||
'2': 'Kelas 2',
|
||||
'3': 'Kelas 3',
|
||||
}
|
||||
|
||||
export const classLevelUpgrades: Record<string, string> = {
|
||||
'1': 'VVIP',
|
||||
'2': 'VIP',
|
||||
'3': 'Kelas 1',
|
||||
'4': 'Kelas 2',
|
||||
'5': 'Kelas 3',
|
||||
'6': 'ICCU',
|
||||
'7': 'ICU',
|
||||
'8': 'Diatas Kelas 1',
|
||||
}
|
||||
|
||||
export const classPaySources: Record<string, string> = {
|
||||
'1': 'Pribadi',
|
||||
'2': 'Pemberi Kerja',
|
||||
'3': 'Asuransi Kesehatan Tambahan',
|
||||
}
|
||||
|
||||
export const procedureTypes: Record<string, string> = {
|
||||
'0': 'Prosedur tidak berkelanjutan',
|
||||
'1': 'Prosedur dan terapi berkelanjutan',
|
||||
}
|
||||
|
||||
export const purposeOfVisits: Record<string, string> = {
|
||||
'0': 'Normal',
|
||||
'1': 'Prosedur',
|
||||
'2': 'Konsul Dokter',
|
||||
}
|
||||
|
||||
export const trafficAccidents: Record<string, string> = {
|
||||
'0': 'Bukan Kecelakaan lalu lintas [BKLL]',
|
||||
'1': 'KLL dan Bukan Kecelakaan Kerja [BKK]',
|
||||
'2': 'KLL dan KK',
|
||||
'3': 'KK',
|
||||
}
|
||||
|
||||
export const supportCodes: Record<string, string> = {
|
||||
'1': 'Radioterapi',
|
||||
'2': 'Kemoterapi',
|
||||
'3': 'Rehabilitasi Medik',
|
||||
'4': 'Rehabilitasi Psikososial',
|
||||
'5': 'Transfusi Darah',
|
||||
'6': 'Pelayanan Gigi',
|
||||
'7': 'Laboratorium',
|
||||
'8': 'USG',
|
||||
'9': 'Farmasi',
|
||||
'10': 'Lain-Lain',
|
||||
'11': 'MRI',
|
||||
'12': 'HEMODIALISA',
|
||||
}
|
||||
|
||||
export const serviceAssessments: Record<string, string> = {
|
||||
'1': 'Poli spesialis tidak tersedia pada hari sebelumnya',
|
||||
'2': 'Jam Poli telah berakhir pada hari sebelumnya',
|
||||
'3': 'Dokter Spesialis yang dimaksud tidak praktek pada hari sebelumnya',
|
||||
'4': 'Atas Instruksi RS',
|
||||
'5': 'Tujuan Kontrol',
|
||||
}
|
||||
|
||||
export const paymentTypes: Record<string, string> = {
|
||||
jkn: 'JKN (Jaminan Kesehatan Nasional)',
|
||||
jkmm: 'JKMM (Jaminan Kesehatan Mandiri)',
|
||||
spm: 'SPM (Sistem Pembayaran Mandiri)',
|
||||
pks: 'PKS (Pembiayaan Kesehatan Sosial)',
|
||||
}
|
||||
|
||||
export const sepRefTypeCodes: Record<string, string> = {
|
||||
internal: 'Rujukan Internal',
|
||||
external: 'Faskes Lain',
|
||||
}
|
||||
|
||||
export const participantGroups: Record<string, string> = {
|
||||
pbi: 'PBI (Penerima Bantuan Iuran)',
|
||||
ppu: 'PPU (Pekerja Penerima Upah)',
|
||||
pbu: 'PBU (Pekerja Bukan Penerima Upah)',
|
||||
bp: 'BP (Bukan Pekerja)',
|
||||
}
|
||||
@@ -25,6 +25,30 @@ export const PAGE_PERMISSIONS = {
|
||||
billing: ['R'],
|
||||
management: ['R'],
|
||||
},
|
||||
'/outpatient/encounter': {
|
||||
doctor: ['C', 'R', 'U', 'D'],
|
||||
nurse: ['C', 'R', 'U', 'D'],
|
||||
admisi: ['R'],
|
||||
pharmacy: ['R'],
|
||||
billing: ['R'],
|
||||
management: ['R'],
|
||||
},
|
||||
'/emergency/encounter': {
|
||||
doctor: ['C', 'R', 'U', 'D'],
|
||||
nurse: ['C', 'R', 'U', 'D'],
|
||||
admisi: ['R'],
|
||||
pharmacy: ['R'],
|
||||
billing: ['R'],
|
||||
management: ['R'],
|
||||
},
|
||||
'/inpatient/encounter': {
|
||||
doctor: ['C', 'R', 'U', 'D'],
|
||||
nurse: ['C', 'R', 'U', 'D'],
|
||||
admisi: ['R'],
|
||||
pharmacy: ['R'],
|
||||
billing: ['R'],
|
||||
management: ['R'],
|
||||
},
|
||||
'/rehab/encounter': {
|
||||
doctor: ['C', 'R', 'U', 'D'],
|
||||
nurse: ['R'],
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { type Base, genBase } from './_base'
|
||||
import type { Unit } from './unit'
|
||||
import type { Subspecialist } from "./subspecialist"
|
||||
|
||||
export interface Specialist extends Base {
|
||||
code: string
|
||||
name: string
|
||||
unit_id?: number | string | null
|
||||
|
||||
unit?: Unit | null
|
||||
subspecialists?: Subspecialist[]
|
||||
}
|
||||
|
||||
export function genSpecialist(): Specialist {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface VclaimSepData {
|
||||
letterDate: string
|
||||
letterNumber: string
|
||||
serviceType: string
|
||||
flow: string
|
||||
medicalRecordNumber: string
|
||||
patientName: string
|
||||
cardNumber: string
|
||||
controlLetterNumber: string
|
||||
controlLetterDate: string
|
||||
clinicDestination: string
|
||||
attendingDoctor: string
|
||||
diagnosis: string
|
||||
careClass: string
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<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: 'Edit Kunjungan',
|
||||
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['/emergency/encounter']
|
||||
|
||||
const { checkRole, hasUpdateAccess } = 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 canUpdate = hasUpdateAccess(roleAccess)
|
||||
|
||||
// Get encounter ID from route params
|
||||
const encounterId = computed(() => {
|
||||
const id = route.params.id
|
||||
return typeof id === 'string' ? parseInt(id) : 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="canUpdate">
|
||||
<ContentEncounterEntry
|
||||
:id="encounterId"
|
||||
class-code="emergency"
|
||||
sub-class-code="emg"
|
||||
form-type="Edit"
|
||||
/>
|
||||
</div>
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<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 Kunjungan',
|
||||
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['/emergency/encounter']
|
||||
|
||||
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">
|
||||
<ContentEncounterEntry
|
||||
:id="0"
|
||||
class-code="emergency"
|
||||
sub-class-code="emg"
|
||||
form-type="Tambah"
|
||||
/>
|
||||
</div>
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,10 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
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: ['system', 'doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||
title: 'Daftar Kunjungan',
|
||||
contentFrame: 'cf-full-width',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/emergency/encounter']
|
||||
|
||||
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 class="p-10 text-center">
|
||||
<div class="mb-5 text-base font-semibold">Hello world!!</div>
|
||||
<div>You are accessing "{{ route.fullPath }}"</div>
|
||||
<div>
|
||||
<div v-if="canRead">
|
||||
<ContentEncounterList
|
||||
class-code="emergency"
|
||||
sub-class-code="emg"
|
||||
type="encounter"
|
||||
/>
|
||||
</div>
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<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: 'Edit Kunjungan',
|
||||
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['/inpatient/encounter']
|
||||
|
||||
const { checkRole, hasUpdateAccess } = 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 canUpdate = hasUpdateAccess(roleAccess)
|
||||
|
||||
// Get encounter ID from route params
|
||||
const encounterId = computed(() => {
|
||||
const id = route.params.id
|
||||
return typeof id === 'string' ? parseInt(id) : 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="canUpdate">
|
||||
<ContentEncounterEntry
|
||||
:id="encounterId"
|
||||
class-code="inpatient"
|
||||
sub-class-code="icu"
|
||||
form-type="Edit"
|
||||
/>
|
||||
</div>
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<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 Kunjungan',
|
||||
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['/inpatient/encounter']
|
||||
|
||||
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">
|
||||
<ContentEncounterEntry
|
||||
:id="0"
|
||||
class-code="inpatient"
|
||||
sub-class-code="icu"
|
||||
form-type="Tambah"
|
||||
/>
|
||||
</div>
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,10 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
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: ['system', 'doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||
title: 'Daftar Kunjungan',
|
||||
contentFrame: 'cf-full-width',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/inpatient/encounter']
|
||||
|
||||
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 class="p-10 text-center">
|
||||
<div class="mb-5 text-base font-semibold">Hello world!!</div>
|
||||
<div>You are accessing "{{ route.fullPath }}"</div>
|
||||
<div>
|
||||
<div v-if="canRead">
|
||||
<ContentEncounterList
|
||||
class-code="inpatient"
|
||||
sub-class-code="vk"
|
||||
type="encounter"
|
||||
/>
|
||||
</div>
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<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: 'Edit Kunjungan',
|
||||
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['/outpatient/encounter']
|
||||
|
||||
const { checkRole, hasUpdateAccess } = 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 canUpdate = hasUpdateAccess(roleAccess)
|
||||
|
||||
// Get encounter ID from route params
|
||||
const encounterId = computed(() => {
|
||||
const id = route.params.id
|
||||
return typeof id === 'string' ? parseInt(id) : 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="canUpdate">
|
||||
<ContentEncounterEntry
|
||||
:id="encounterId"
|
||||
class-code="ambulatory"
|
||||
sub-class-code="reg"
|
||||
form-type="Edit"
|
||||
/>
|
||||
</div>
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<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 Kunjungan',
|
||||
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['/outpatient/encounter']
|
||||
|
||||
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">
|
||||
<ContentEncounterEntry
|
||||
:id="0"
|
||||
class-code="ambulatory"
|
||||
sub-class-code="reg"
|
||||
form-type="Tambah"
|
||||
/>
|
||||
</div>
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,10 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
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: ['system', 'doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||
title: 'Daftar Kunjungan',
|
||||
contentFrame: 'cf-full-width',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/outpatient/encounter']
|
||||
|
||||
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 class="p-10 text-center">
|
||||
<div class="mb-5 text-base font-semibold">Hello world!!</div>
|
||||
<div>You are accessing "{{ route.fullPath }}"</div>
|
||||
<div>
|
||||
<div v-if="canRead">
|
||||
<ContentEncounterList
|
||||
class-code="ambulatory"
|
||||
sub-class-code="reg"
|
||||
type="encounter"
|
||||
/>
|
||||
</div>
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
definePageMeta({
|
||||
middleware: ['rbac'],
|
||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||
title: 'Tambah Kunjungan',
|
||||
title: 'Edit Kunjungan',
|
||||
contentFrame: 'cf-full-width',
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ useHead({
|
||||
|
||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
|
||||
|
||||
const { checkRole, hasCreateAccess } = useRBAC()
|
||||
const { checkRole, hasUpdateAccess } = useRBAC()
|
||||
|
||||
// Check if user has access to this page
|
||||
const hasAccess = checkRole(roleAccess)
|
||||
@@ -30,12 +30,26 @@ if (!hasAccess) {
|
||||
}
|
||||
|
||||
// Define permission-based computed properties
|
||||
const canCreate = hasCreateAccess(roleAccess)
|
||||
const canUpdate = hasUpdateAccess(roleAccess)
|
||||
|
||||
// Get encounter ID from route params
|
||||
const encounterId = computed(() => {
|
||||
const id = route.params.id
|
||||
return typeof id === 'string' ? parseInt(id) : 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="canCreate">
|
||||
<ContentEncounterEntry :id="1" form-type="Edit" />
|
||||
<div v-if="canUpdate">
|
||||
<ContentEncounterEntry
|
||||
:id="encounterId"
|
||||
class-code="ambulatory"
|
||||
sub-class-code="rehab"
|
||||
form-type="Edit"
|
||||
/>
|
||||
</div>
|
||||
<Error v-else :status-code="403" />
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -35,7 +35,15 @@ const canCreate = hasCreateAccess(roleAccess)
|
||||
|
||||
<template>
|
||||
<div v-if="canCreate">
|
||||
<ContentEncounterEntry form-type="Tambah" />
|
||||
<ContentEncounterEntry
|
||||
:id="0"
|
||||
class-code="ambulatory"
|
||||
sub-class-code="rehab"
|
||||
form-type="Tambah"
|
||||
/>
|
||||
</div>
|
||||
<Error v-else :status-code="403" />
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -33,8 +33,15 @@ const canRead = hasReadAccess(roleAccess)
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="canRead">
|
||||
<ContentEncounterList type="encounter" />
|
||||
<ContentEncounterList
|
||||
class-code="ambulatory"
|
||||
sub-class-code="rehab"
|
||||
type="encounter"
|
||||
/>
|
||||
</div>
|
||||
<Error v-else :status-code="403" />
|
||||
<Error
|
||||
v-else
|
||||
:status-code="403"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const ERROR_MESSAGES = {
|
||||
required: {
|
||||
sepDate: 'Tanggal wajib diisi',
|
||||
serviceType: 'Jenis Pelayanan wajib diisi',
|
||||
admissionType: 'Jenis Pendaftaran wajib diisi',
|
||||
cardNumber: 'No. Kartu BPJS wajib diisi',
|
||||
nationalId: 'Nomor ID wajib diisi',
|
||||
medicalRecordNumber: 'Nomor Rekam Medis wajib diisi',
|
||||
patientName: 'Nama wajib diisi',
|
||||
phoneNumber: 'Nomor Telepon wajib diisi',
|
||||
referralLetterNumber: 'Nomor Surat Kontrol wajib diisi',
|
||||
referralLetterDate: 'Tanggal Surat Kontrol wajib diisi',
|
||||
fromClinic: 'Faskes Asal wajib diisi',
|
||||
destinationClinic: 'Klinik Tujuan wajib diisi',
|
||||
attendingDoctor: 'Dokter wajib diisi',
|
||||
initialDiagnosis: 'Diagnosa Awal wajib diisi',
|
||||
cob: 'COB wajib diisi',
|
||||
cataract: 'Katarak wajib diisi',
|
||||
clinicExcecutive: 'Klinkik eksekutif wajib diisi',
|
||||
subSpecialistId: 'Subspesialis wajib diisi',
|
||||
procedureType: 'Jenis Prosedur wajib diisi',
|
||||
supportCode: 'Kode Penunjang wajib diisi',
|
||||
note: 'Catatan wajib diisi',
|
||||
trafficAccident: 'Kejadian lalu lintas wajib diisi',
|
||||
purposeOfVisit: 'Tujuan Kunjungan wajib diisi',
|
||||
serviceAssessment: 'Assemen Pelayanan wajib diisi',
|
||||
lpNumber: 'Nomor LP wajib diisi',
|
||||
accidentDate: 'Tanggal Kejadian lalu lintas wajib diisi',
|
||||
accidentNote: 'Keterangan Kejadian lalu lintas wajib diisi',
|
||||
accidentProvince: 'Provinsi Kejadian lalu lintas wajib diisi',
|
||||
accidentCity: 'Kota Kejadian lalu lintas wajib diisi',
|
||||
accidentDistrict: 'Kecamatan Kejadian lalu lintas wajib diisi',
|
||||
suplesi: 'Suplesi wajib diisi',
|
||||
suplesiNumber: 'Nomor Suplesi wajib diisi',
|
||||
classLevel: 'Kelas Rawat wajib diisi',
|
||||
classLevelUpgrade: 'Kelas Rawat Naik wajib diisi',
|
||||
classPaySource: 'Pembiayaan wajib diisi',
|
||||
responsiblePerson: 'Penanggung Jawab wajib diisi',
|
||||
},
|
||||
}
|
||||
|
||||
const IntegrationBpjsSchema = z
|
||||
.object({
|
||||
sepDate: z.string({ required_error: ERROR_MESSAGES.required.sepDate }).min(1, ERROR_MESSAGES.required.sepDate),
|
||||
serviceType: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.serviceType })
|
||||
.min(1, ERROR_MESSAGES.required.serviceType)
|
||||
.optional(),
|
||||
admissionType: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.admissionType })
|
||||
.min(1, ERROR_MESSAGES.required.admissionType),
|
||||
cardNumber: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.cardNumber })
|
||||
.min(1, ERROR_MESSAGES.required.cardNumber),
|
||||
nationalId: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.nationalId })
|
||||
.min(1, ERROR_MESSAGES.required.nationalId),
|
||||
medicalRecordNumber: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.medicalRecordNumber })
|
||||
.min(1, ERROR_MESSAGES.required.medicalRecordNumber),
|
||||
patientName: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.patientName })
|
||||
.min(1, ERROR_MESSAGES.required.patientName),
|
||||
phoneNumber: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.phoneNumber })
|
||||
.min(1, ERROR_MESSAGES.required.phoneNumber),
|
||||
referralLetterNumber: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.referralLetterNumber })
|
||||
.min(1, ERROR_MESSAGES.required.referralLetterNumber).optional(),
|
||||
referralLetterDate: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.referralLetterDate })
|
||||
.min(1, ERROR_MESSAGES.required.referralLetterDate).optional(),
|
||||
fromClinic: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.fromClinic })
|
||||
.min(1, ERROR_MESSAGES.required.fromClinic)
|
||||
.optional(),
|
||||
destinationClinic: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.destinationClinic })
|
||||
.min(1, ERROR_MESSAGES.required.destinationClinic),
|
||||
attendingDoctor: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.attendingDoctor })
|
||||
.min(1, ERROR_MESSAGES.required.attendingDoctor),
|
||||
initialDiagnosis: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.initialDiagnosis })
|
||||
.min(1, ERROR_MESSAGES.required.initialDiagnosis),
|
||||
cob: z.string({ required_error: ERROR_MESSAGES.required.cob }).min(1, ERROR_MESSAGES.required.cob),
|
||||
cataract: z.string({ required_error: ERROR_MESSAGES.required.cataract }).min(1, ERROR_MESSAGES.required.cataract),
|
||||
clinicExcecutive: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.clinicExcecutive })
|
||||
.min(1, ERROR_MESSAGES.required.clinicExcecutive),
|
||||
subSpecialistId: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.subSpecialistId })
|
||||
.min(1, ERROR_MESSAGES.required.subSpecialistId)
|
||||
.optional(),
|
||||
procedureType: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.procedureType })
|
||||
.min(1, ERROR_MESSAGES.required.procedureType)
|
||||
.optional(),
|
||||
supportCode: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.supportCode })
|
||||
.min(1, ERROR_MESSAGES.required.supportCode)
|
||||
.optional(),
|
||||
note: z.string({ required_error: ERROR_MESSAGES.required.note }).min(1, ERROR_MESSAGES.required.note).optional(),
|
||||
trafficAccident: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.trafficAccident })
|
||||
.min(1, ERROR_MESSAGES.required.trafficAccident)
|
||||
.optional(),
|
||||
purposeOfVisit: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.purposeOfVisit })
|
||||
.min(1, ERROR_MESSAGES.required.purposeOfVisit)
|
||||
.optional(),
|
||||
serviceAssessment: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.serviceAssessment })
|
||||
.min(1, ERROR_MESSAGES.required.serviceAssessment)
|
||||
.optional(),
|
||||
lpNumber: z.string({ required_error: ERROR_MESSAGES.required.lpNumber }).optional(),
|
||||
accidentDate: z.string({ required_error: ERROR_MESSAGES.required.accidentDate }).optional(),
|
||||
accidentNote: z.string({ required_error: ERROR_MESSAGES.required.accidentNote }).optional(),
|
||||
accidentProvince: z.string({ required_error: ERROR_MESSAGES.required.accidentProvince }).optional(),
|
||||
accidentCity: z.string({ required_error: ERROR_MESSAGES.required.accidentCity }).optional(),
|
||||
accidentDistrict: z.string({ required_error: ERROR_MESSAGES.required.accidentDistrict }).optional(),
|
||||
suplesi: z.string({ required_error: ERROR_MESSAGES.required.suplesi }).optional(),
|
||||
suplesiNumber: z.string({ required_error: ERROR_MESSAGES.required.suplesiNumber }).optional(),
|
||||
classLevel: z.string({ required_error: ERROR_MESSAGES.required.classLevel }).optional(),
|
||||
classLevelUpgrade: z.string({ required_error: ERROR_MESSAGES.required.classLevelUpgrade }).optional(),
|
||||
classPaySource: z.string({ required_error: ERROR_MESSAGES.required.classPaySource }).optional(),
|
||||
responsiblePerson: z.string({ required_error: ERROR_MESSAGES.required.responsiblePerson }).optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.trafficAccident && data.trafficAccident.trim() !== '') {
|
||||
return data.accidentDate && data.accidentDate.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.accidentDate,
|
||||
path: ['accidentDate'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.trafficAccident && data.trafficAccident.trim() !== '') {
|
||||
return data.accidentProvince && data.accidentProvince.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.accidentProvince,
|
||||
path: ['accidentProvince'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.trafficAccident && data.trafficAccident.trim() !== '') {
|
||||
return data.accidentCity && data.accidentCity.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.accidentCity,
|
||||
path: ['accidentCity'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.trafficAccident && data.trafficAccident.trim() !== '') {
|
||||
return data.accidentDistrict && data.accidentDistrict.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.accidentDistrict,
|
||||
path: ['accidentDistrict'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.trafficAccident && data.trafficAccident.trim() !== '') {
|
||||
return data.suplesi && data.suplesi.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.suplesi,
|
||||
path: ['suplesi'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.trafficAccident && data.trafficAccident.trim() !== '' && data.suplesi?.trim() === 'yes') {
|
||||
return data.suplesiNumber && data.suplesiNumber.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.suplesiNumber,
|
||||
path: ['suplesiNumber'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.serviceType === '1') {
|
||||
return data.classLevel && data.classLevel.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.classLevel,
|
||||
path: ['classLevel'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.serviceType === '1') {
|
||||
return data.classLevelUpgrade && data.classLevelUpgrade.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.classLevelUpgrade,
|
||||
path: ['classLevelUpgrade'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.serviceType === '1' && data.classLevelUpgrade?.trim() !== '') {
|
||||
return data.classPaySource && data.classPaySource.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.classPaySource,
|
||||
path: ['classPaySource'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.serviceType === '1' && data.classPaySource?.trim() !== '') {
|
||||
return data.responsiblePerson && data.responsiblePerson.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.responsiblePerson,
|
||||
path: ['responsiblePerson'],
|
||||
},
|
||||
)
|
||||
|
||||
type IntegrationBpjsFormData = z.infer<typeof IntegrationBpjsSchema>
|
||||
|
||||
export { IntegrationBpjsSchema }
|
||||
export type { IntegrationBpjsFormData }
|
||||
@@ -0,0 +1,133 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const ERROR_MESSAGES = {
|
||||
required: {
|
||||
doctorId: 'Dokter wajib diisi',
|
||||
registerDate: 'Tanggal Daftar wajib diisi',
|
||||
paymentType: 'Jenis Pembayaran wajib diisi',
|
||||
subSpecialistId: 'Subspesialis wajib diisi',
|
||||
patientCategory: 'Kelompok Peserta wajib diisi',
|
||||
cardNumber: 'No. Kartu BPJS wajib diisi',
|
||||
sepType: 'Jenis SEP wajib diisi',
|
||||
sepNumber: 'No. SEP wajib diisi',
|
||||
},
|
||||
}
|
||||
|
||||
const ACCEPTED_UPLOAD_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
|
||||
|
||||
const IntegrationEncounterSchema = z
|
||||
.object({
|
||||
// Patient data (readonly, populated from selected patient)
|
||||
patientName: z.string().optional(),
|
||||
nationalIdentity: z.string().optional(),
|
||||
medicalRecordNumber: z.string().optional(),
|
||||
|
||||
// Visit data
|
||||
doctorId: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.doctorId })
|
||||
.min(1, ERROR_MESSAGES.required.doctorId),
|
||||
subSpecialistId: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.subSpecialistId })
|
||||
.min(1, ERROR_MESSAGES.required.subSpecialistId)
|
||||
.optional(),
|
||||
registerDate: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.registerDate })
|
||||
.min(1, ERROR_MESSAGES.required.registerDate),
|
||||
paymentType: z
|
||||
.string({ required_error: ERROR_MESSAGES.required.paymentType })
|
||||
.min(1, ERROR_MESSAGES.required.paymentType),
|
||||
|
||||
// BPJS related fields
|
||||
patientCategory: z
|
||||
.string()
|
||||
.min(1, ERROR_MESSAGES.required.patientCategory)
|
||||
.optional(),
|
||||
cardNumber: z
|
||||
.string()
|
||||
.min(1, ERROR_MESSAGES.required.cardNumber)
|
||||
.optional(),
|
||||
sepType: z
|
||||
.string()
|
||||
.min(1, ERROR_MESSAGES.required.sepType)
|
||||
.optional(),
|
||||
sepNumber: z
|
||||
.string()
|
||||
.min(1, ERROR_MESSAGES.required.sepNumber)
|
||||
.optional(),
|
||||
|
||||
// File uploads
|
||||
sepFile: z
|
||||
.any()
|
||||
.optional()
|
||||
.refine((f) => !f || f instanceof File, { message: 'Harus berupa file yang valid' })
|
||||
.refine((f) => !f || ACCEPTED_UPLOAD_TYPES.includes(f.type), {
|
||||
message: 'Format file harus JPG, PNG, atau PDF',
|
||||
})
|
||||
.refine((f) => !f || f.size <= 1 * 1024 * 1024, { message: 'Maksimal 1MB' }),
|
||||
sippFile: z
|
||||
.any()
|
||||
.optional()
|
||||
.refine((f) => !f || f instanceof File, { message: 'Harus berupa file yang valid' })
|
||||
.refine((f) => !f || ACCEPTED_UPLOAD_TYPES.includes(f.type), {
|
||||
message: 'Format file harus JPG, PNG, atau PDF',
|
||||
})
|
||||
.refine((f) => !f || f.size <= 1 * 1024 * 1024, { message: 'Maksimal 1MB' }),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// If payment type is jkn, then patient category is required
|
||||
if (data.paymentType === 'jkn') {
|
||||
return data.patientCategory && data.patientCategory.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.patientCategory,
|
||||
path: ['patientCategory'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// If payment type is jkn, then card number is required
|
||||
if (data.paymentType === 'jkn') {
|
||||
return data.cardNumber && data.cardNumber.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.cardNumber,
|
||||
path: ['cardNumber'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// If payment type is jkn, then SEP type is required
|
||||
if (data.paymentType === 'jkn') {
|
||||
return data.sepType && data.sepType.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.sepType,
|
||||
path: ['sepType'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// If payment type is jkn and SEP type is selected, then SEP number is required
|
||||
if (data.paymentType === 'jkn' && data.sepType && data.sepType.trim() !== '') {
|
||||
return data.sepNumber && data.sepNumber.trim() !== ''
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: ERROR_MESSAGES.required.sepNumber,
|
||||
path: ['sepNumber'],
|
||||
},
|
||||
)
|
||||
|
||||
type IntegrationEncounterFormData = z.infer<typeof IntegrationEncounterSchema>
|
||||
|
||||
export { IntegrationEncounterSchema }
|
||||
export type { IntegrationEncounterFormData }
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
import type { Doctor } from "~/models/doctor";
|
||||
|
||||
// Types
|
||||
import type { Doctor } from '~/models/doctor'
|
||||
|
||||
const path = '/api/v1/doctor'
|
||||
const name = 'device'
|
||||
const name = 'doctor'
|
||||
|
||||
export function create(data: any) {
|
||||
return base.create(path, data, name)
|
||||
@@ -31,8 +33,8 @@ export async function getValueLabelList(params: any = null): Promise<{ value: st
|
||||
if (result.success) {
|
||||
const resultData = result.body?.data || []
|
||||
data = resultData.map((item: Doctor) => ({
|
||||
value: item.id,
|
||||
label: item.employee.person.name,
|
||||
value: item.id ? String(item.id) : '',
|
||||
label: item.employee?.person?.name || '',
|
||||
}))
|
||||
}
|
||||
return data
|
||||
|
||||
@@ -40,6 +40,22 @@ export async function getPatientDetail(id: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPatientByIdentifier(search: string) {
|
||||
try {
|
||||
const urlPath = search.length === 16 ? `by-resident-identity/search=${encodeURIComponent(search)}` : `/search/${search}`
|
||||
const url = `${mainUrl}/${urlPath}`
|
||||
const resp = await xfetch(url, 'GET')
|
||||
const result: any = {}
|
||||
result.success = resp.success
|
||||
result.body = (resp.body as Record<string, any>) || {}
|
||||
result.type = urlPath.includes('by-resident-identity') ? 'resident-identity' : 'identity'
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Error fetching patient by identifier:', error)
|
||||
throw new Error('Failed to get patient by identifier')
|
||||
}
|
||||
}
|
||||
|
||||
export async function postPatient(record: any) {
|
||||
try {
|
||||
const resp = await xfetch(mainUrl, 'POST', record)
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as base from './_crud-base'
|
||||
|
||||
// Types
|
||||
import type { Specialist } from '~/models/specialist'
|
||||
import type { TreeItem } from '~/models/_base'
|
||||
|
||||
const path = '/api/v1/specialist'
|
||||
const name = 'specialist'
|
||||
@@ -40,3 +41,20 @@ export async function getValueLabelList(params: any = null): Promise<{ value: st
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert specialist response to TreeItem[] with subspecialist children
|
||||
* @param specialists Array of specialist objects from API
|
||||
* @returns TreeItem[]
|
||||
*/
|
||||
export function getValueTreeItems(specialists: any[], byCode = true): TreeItem[] {
|
||||
return specialists.map((specialist: Specialist) => ({
|
||||
value: byCode ? String(specialist.code) : String(specialist.id),
|
||||
label: specialist.name,
|
||||
hasChildren: Array.isArray(specialist.subspecialists) && specialist.subspecialists.length > 0,
|
||||
children:
|
||||
Array.isArray(specialist.subspecialists) && specialist.subspecialists.length > 0
|
||||
? getValueTreeItems(specialist.subspecialists)
|
||||
: undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim-swagger/RencanaKontrol'
|
||||
const name = 'rencana-kontrol'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params?.letterNumber && params.mode === 'by-control') {
|
||||
url += `/noSuratKontrol/${params.letterNumber}`
|
||||
}
|
||||
if (params?.letterNumber && params.mode === 'by-card') {
|
||||
url += `/noka/${params.letterNumber}`
|
||||
}
|
||||
if (params?.letterNumber && params.mode === 'by-sep') {
|
||||
url += `/${params.letterNumber}`
|
||||
}
|
||||
if (params?.letterNumber && params.mode === 'by-schedule') {
|
||||
url += `/jadwalDokter?jeniskontrol=${params.controlType}&kodepoli=${params.poliCode}&tanggalkontrol=${params.controlDate}`
|
||||
delete params.controlType
|
||||
delete params.poliCode
|
||||
delete params.controlDate
|
||||
}
|
||||
if (params) {
|
||||
delete params.letterNumber
|
||||
delete params.mode
|
||||
}
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/reference/diagnose-prb'
|
||||
const name = 'diagnose-referral'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
return base.getList(path, params, name)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/reference/diagnose'
|
||||
const name = 'diagnose'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params && params?.diagnosa) {
|
||||
url += `/${params.diagnosa}`
|
||||
delete params.diagnosa
|
||||
}
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
|
||||
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
|
||||
let data: { value: string; label: string }[] = []
|
||||
const result = await getList(params)
|
||||
if (result.success) {
|
||||
const resultData = result.body?.response?.diagnosa || []
|
||||
const resultUnique = [...new Map(resultData.map((item: any) => [item.kode, item])).values()]
|
||||
data = resultUnique.map((item: any) => ({
|
||||
value: item.kode ? String(item.kode) : '',
|
||||
label: `${item.kode} - ${item.nama}`,
|
||||
}))
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/reference/responsible-doctor'
|
||||
const name = 'responsible-doctor'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params?.serviceType) {
|
||||
url += `/${params.serviceType}`
|
||||
delete params.serviceType
|
||||
}
|
||||
if (params?.serviceDate) {
|
||||
url += `/${params.serviceDate}`
|
||||
delete params.serviceDate
|
||||
}
|
||||
if (params?.specialistCode || (Number(params.specialistCode) === 0)) {
|
||||
url += `/${params.specialistCode}`
|
||||
delete params.specialistCode
|
||||
}
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
|
||||
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
|
||||
let data: { value: string; label: string }[] = []
|
||||
const result = await getList(params)
|
||||
if (result.success) {
|
||||
const resultData = result.body?.response?.list || []
|
||||
const resultUnique = [...new Map(resultData.map((item: any) => [item.kode, item])).values()]
|
||||
data = resultUnique.map((item: any) => ({
|
||||
value: item.kode ? String(item.kode) : '',
|
||||
label: `${item.kode} - ${item.nama}`,
|
||||
}))
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/reference/healthcare'
|
||||
const name = 'healthcare'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params?.healthcare) {
|
||||
url += `/${params.healthcare}`
|
||||
delete params.healthcare
|
||||
}
|
||||
if (params?.healthcareType) {
|
||||
url += `/${params.healthcareType}`
|
||||
delete params.healthcareType
|
||||
}
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
|
||||
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
|
||||
let data: { value: string; label: string }[] = []
|
||||
const result = await getList(params)
|
||||
if (result.success) {
|
||||
const resultData = result.body?.response?.faskes || []
|
||||
const resultUnique = [...new Map(resultData.map((item: any) => [item.kode, item])).values()]
|
||||
data = resultUnique.map((item: any) => ({
|
||||
value: item.kode ? String(item.kode) : '',
|
||||
label: `${item.kode} - ${item.nama}`,
|
||||
}))
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/reference/medicine'
|
||||
const name = 'medicine'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
return base.getList(path, params, name)
|
||||
}
|
||||
|
||||
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
|
||||
let data: { value: string; label: string }[] = []
|
||||
const result = await getList(params)
|
||||
if (result.success) {
|
||||
const resultData = result.body?.response?.list || []
|
||||
data = resultData.map((item: any) => ({
|
||||
value: item.kode ? String(item.kode) : '',
|
||||
label: item.nama,
|
||||
}))
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/member'
|
||||
const name = 'member'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params?.number && params.mode === 'by-identity') {
|
||||
url += `/nik/${params.number}/${params.date}`
|
||||
}
|
||||
if (params?.number && params.mode === 'by-card') {
|
||||
url += `/bpjs/${params.number}/${params.date}`
|
||||
}
|
||||
if (params) {
|
||||
delete params.number
|
||||
delete params.mode
|
||||
delete params.date
|
||||
}
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/monitoring/hist'
|
||||
const name = 'monitoring-history'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params && params?.cardNumber) {
|
||||
url += `/${params.cardNumber}/${params.startDate}/${params.endDate}`
|
||||
delete params.cardNumber
|
||||
delete params.startDate
|
||||
delete params.endDate
|
||||
}
|
||||
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/monitoring/visit'
|
||||
const name = 'monitoring-visit'
|
||||
|
||||
const dummyResponse = {
|
||||
metaData: {
|
||||
code: '200',
|
||||
message: 'Sukses',
|
||||
},
|
||||
response: {
|
||||
sep: [
|
||||
{
|
||||
diagnosa: 'K65.0',
|
||||
jnsPelayanan: 'R.Inap',
|
||||
kelasRawat: '2',
|
||||
nama: 'HANIF ABDURRAHMAN',
|
||||
noKartu: '0001819122189',
|
||||
noSep: '0301R00110170000004',
|
||||
noRujukan: '0301U01108180200084',
|
||||
poli: null,
|
||||
tglPlgSep: '2017-10-03',
|
||||
tglSep: '2017-10-01',
|
||||
},
|
||||
{
|
||||
diagnosa: 'I50.0',
|
||||
jnsPelayanan: 'R.Inap',
|
||||
kelasRawat: '3',
|
||||
nama: 'ASRIZAL',
|
||||
noKartu: '0002283324674',
|
||||
noSep: '0301R00110170000005',
|
||||
noRujukan: '0301U01108180200184',
|
||||
poli: null,
|
||||
tglPlgSep: '2017-10-10',
|
||||
tglSep: '2017-10-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export async function getList(params: any = null) {
|
||||
try {
|
||||
let url = path
|
||||
if (params?.date && params.serviceType) {
|
||||
url += `/${params.date}/${params.serviceType}`
|
||||
}
|
||||
if (params) {
|
||||
delete params.date
|
||||
delete params.serviceType
|
||||
}
|
||||
const resp = await base.getList(url, params, name)
|
||||
|
||||
// Jika success false, return dummy response
|
||||
if (!resp.success || !resp.body?.response) {
|
||||
return {
|
||||
success: true,
|
||||
body: dummyResponse,
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
} catch (error) {
|
||||
// Jika terjadi error, return dummy response
|
||||
console.error(`Error fetching ${name}s:`, error)
|
||||
return {
|
||||
success: true,
|
||||
body: dummyResponse,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim-swagger/Rujukan/RS'
|
||||
const name = 'rujukan-rumah-sakit'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params?.letterNumber) {
|
||||
url += `/${params.letterNumber}`
|
||||
}
|
||||
if (params) {
|
||||
delete params.letterNumber
|
||||
}
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim-swagger/RujukanKhusus'
|
||||
const name = 'rujukan-khusus'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params?.letterNumber) {
|
||||
url += `?noRujukan=${params.letterNumber}`
|
||||
}
|
||||
if (params) {
|
||||
delete params.letterNumber
|
||||
}
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/reference/regency'
|
||||
const name = 'cities'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params?.province) {
|
||||
url += `/${params.province}`
|
||||
delete params.province
|
||||
}
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
|
||||
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
|
||||
let data: { value: string; label: string }[] = []
|
||||
const result = await getList(params)
|
||||
if (result.success) {
|
||||
const resultData = result.body?.response?.list || []
|
||||
data = resultData.map((item: any) => ({
|
||||
value: item.kode ? String(item.kode) : '',
|
||||
label: item.nama,
|
||||
}))
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/reference/district'
|
||||
const name = 'districts'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params?.city) {
|
||||
url += `/${params.city}`
|
||||
delete params.city
|
||||
}
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
|
||||
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
|
||||
let data: { value: string; label: string }[] = []
|
||||
const result = await getList(params)
|
||||
if (result.success) {
|
||||
const resultData = result.body?.response?.list || []
|
||||
data = resultData.map((item: any) => ({
|
||||
value: item.kode ? String(item.kode) : '',
|
||||
label: item.nama,
|
||||
}))
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/reference/province'
|
||||
const name = 'provinces'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
return base.getList(path, params, name)
|
||||
}
|
||||
|
||||
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
|
||||
let data: { value: string; label: string }[] = []
|
||||
const result = await getList(params)
|
||||
if (result.success) {
|
||||
const resultData = result.body?.response?.list || []
|
||||
data = resultData.map((item: any) => ({
|
||||
value: item.kode ? String(item.kode) : '',
|
||||
label: item.nama,
|
||||
}))
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
// Types
|
||||
import type { IntegrationBpjsFormData } from '~/schemas/integration-bpjs.schema'
|
||||
|
||||
const path = '/api/vclaim-swagger/sep'
|
||||
const name = 'sep'
|
||||
|
||||
export function create(data: any) {
|
||||
return base.create(path, data, name)
|
||||
}
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params?.number) {
|
||||
url += `/${params.number}`
|
||||
delete params.number
|
||||
}
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
|
||||
export function makeSepData(
|
||||
data: IntegrationBpjsFormData & {
|
||||
referralFrom?: string
|
||||
referralTo?: string
|
||||
referralLetterDate?: string
|
||||
referralLetterNumber?: string
|
||||
},
|
||||
) {
|
||||
const content = {
|
||||
noKartu: data.cardNumber || '',
|
||||
tglSep: data.sepDate,
|
||||
ppkPelayanan: data.fromClinic || '',
|
||||
jnsPelayanan: data.admissionType ? String(data.admissionType) : '1',
|
||||
noMR: data.medicalRecordNumber || '',
|
||||
catatan: data.note || '',
|
||||
diagAwal: data.initialDiagnosis || '',
|
||||
poli: {
|
||||
tujuan: data.destinationClinic || '',
|
||||
eksekutif: data.clinicExcecutive === 'yes' ? '1' : '0',
|
||||
},
|
||||
cob: {
|
||||
cob: data.cob === 'yes' ? '1' : '0',
|
||||
},
|
||||
katarak: {
|
||||
katarak: data.cataract === 'yes' ? '1' : '0',
|
||||
},
|
||||
tujuanKunj: data.purposeOfVisit || '',
|
||||
flagProcedure: data.procedureType || '',
|
||||
kdPenunjang: data.supportCode || '',
|
||||
assesmentPel: data.serviceAssessment || '',
|
||||
skdp: {
|
||||
noSurat: ['3'].includes(data.admissionType) ? data.referralLetterNumber : '',
|
||||
kodeDPJP: ['3'].includes(data.admissionType)? data.attendingDoctor : '',
|
||||
},
|
||||
rujukan: {
|
||||
asalRujukan: ['2'].includes(data.admissionType) ? data?.referralFrom || '' : '',
|
||||
tglRujukan: ['2'].includes(data.admissionType) ? data?.referralLetterDate || '' : '',
|
||||
noRujukan: ['2'].includes(data.admissionType) ? data?.referralLetterNumber || '' : '',
|
||||
ppkRujukan: ['2'].includes(data.admissionType) ? data?.referralTo || '' : '',
|
||||
},
|
||||
klsRawat: {
|
||||
klsRawatHak: data.classLevel || '',
|
||||
klsRawatNaik: data.classLevelUpgrade || '',
|
||||
pembiayaan: data.classPaySource || '',
|
||||
penanggungJawab: data.responsiblePerson || '',
|
||||
},
|
||||
dpjpLayan: data.attendingDoctor || '',
|
||||
noTelp: data.phoneNumber || '',
|
||||
user: data.patientName || '',
|
||||
jaminan: {
|
||||
lakaLantas: data.trafficAccident || '0',
|
||||
noLP: data.lpNumber || '',
|
||||
penjamin: {
|
||||
tglKejadian: data.accidentDate || '',
|
||||
keterangan: data.accidentNote || '',
|
||||
suplesi: {
|
||||
suplesi: data.suplesi === 'yes' ? '1' : '0',
|
||||
noSepSuplesi: data.suplesiNumber || '',
|
||||
lokasiLaka: {
|
||||
kdPropinsi: data.accidentProvince || '',
|
||||
kdKabupaten: data.accidentCity || '',
|
||||
kdKecamatan: data.accidentDistrict || '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return {
|
||||
request: {
|
||||
t_sep: content,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
const path = '/api/vclaim/v1/reference/unit'
|
||||
const name = 'unit'
|
||||
|
||||
export function getList(params: any = null) {
|
||||
let url = path
|
||||
if (params?.unitCode) {
|
||||
url += `/${params.unitCode}`
|
||||
delete params.unitCode
|
||||
}
|
||||
return base.getList(url, params, name)
|
||||
}
|
||||
|
||||
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
|
||||
let data: { value: string; label: string }[] = []
|
||||
const result = await getList(params)
|
||||
if (result.success) {
|
||||
const resultData = result.body?.response?.faskes || []
|
||||
const resultUnique = [...new Map(resultData.map((item: any) => [item.kode, item])).values()]
|
||||
data = resultUnique.map((item: any) => ({
|
||||
value: item.kode ? String(item.kode) : '',
|
||||
label: `${item.kode} - ${item.nama}`,
|
||||
}))
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -5,8 +5,12 @@ export default defineNuxtConfig({
|
||||
devtools: { enabled: true },
|
||||
runtimeConfig: {
|
||||
API_ORIGIN: process.env.NUXT_API_ORIGIN || 'http://localhost:3000',
|
||||
VCLAIM: process.env.NUXT_API_VCLAIM || 'http://localhost:3000',
|
||||
VCLAIM_SWAGGER: process.env.NUXT_API_VCLAIM_SWAGGER || 'http://localhost:3000',
|
||||
public: {
|
||||
API_ORIGIN: process.env.NUXT_API_ORIGIN || 'http://localhost:3000',
|
||||
VCLAIM: process.env.NUXT_API_VCLAIM || 'http://localhost:3000',
|
||||
VCLAIM_SWAGGER: process.env.NUXT_API_VCLAIM_SWAGGER || 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
ssr: false,
|
||||
|
||||
+27
-6
@@ -5,11 +5,20 @@ export default defineEventHandler(async (event) => {
|
||||
const headers = getRequestHeaders(event)
|
||||
const url = getRequestURL(event)
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const apiOrigin = config.public.API_ORIGIN
|
||||
const pathname = url.pathname.replace(/^\/api/, '')
|
||||
|
||||
const targetUrl = apiOrigin + pathname + (url.search || '')
|
||||
const apiOrigin = config.public.API_ORIGIN
|
||||
const apiVclaim = config.public.VCLAIM
|
||||
const apiVclaimSwagger = config.public.VCLAIM_SWAGGER
|
||||
const pathname = url.pathname.replace(/^\/api/, '')
|
||||
const isVclaim = pathname.includes('/vclaim')
|
||||
|
||||
let targetUrl = apiOrigin + pathname + (url.search || '')
|
||||
if (pathname.includes('/vclaim')) {
|
||||
targetUrl = apiVclaim + pathname.replace('/vclaim', '') + (url.search || '')
|
||||
}
|
||||
if (pathname.includes('/vclaim-swagger')) {
|
||||
targetUrl = apiVclaimSwagger + pathname.replace('/vclaim-swagger', '') + (url.search || '')
|
||||
}
|
||||
|
||||
const verificationId = headers['verification-id'] as string | undefined
|
||||
let bearer = ''
|
||||
@@ -21,8 +30,10 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const forwardHeaders = new Headers()
|
||||
if (headers['content-type']) forwardHeaders.set('Content-Type', headers['content-type'])
|
||||
forwardHeaders.set('Authorization', `Bearer ${bearer}`)
|
||||
if (!isVclaim) {
|
||||
if (headers['content-type']) forwardHeaders.set('Content-Type', headers['content-type'])
|
||||
forwardHeaders.set('Authorization', `Bearer ${bearer}`)
|
||||
}
|
||||
|
||||
let body: any
|
||||
if (['POST', 'PATCH'].includes(method!)) {
|
||||
@@ -41,5 +52,15 @@ export default defineEventHandler(async (event) => {
|
||||
body,
|
||||
})
|
||||
|
||||
if (isVclaim) {
|
||||
const resClone = res.clone()
|
||||
const responseBody = await resClone.json()
|
||||
return {
|
||||
status: resClone.status,
|
||||
headers: resClone.headers,
|
||||
...responseBody,
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user