Merge branch 'dev' into feat/education-assessment-79

This commit is contained in:
Khafid Prayoga
2025-12-08 12:21:06 +07:00
committed by GitHub
44 changed files with 3034 additions and 388 deletions
+1
View File
@@ -1,4 +1,5 @@
NUXT_MAIN_API_ORIGIN=
NUXT_BPJS_API_ORIGIN=
NUXT_API_VCLAIM_SWAGGER= # https://vclaim-api.multy.chat
NUXT_SYNC_API_ORIGIN=
NUXT_API_ORIGIN=
@@ -0,0 +1,58 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [{}, {}, {}, { width: 50 }],
headers: [[
{ label: 'Tgl. Order' },
{ label: 'No. Order' },
{ label: 'Jadwal Pemeriksaan' },
{ label: 'Lokalisasi' },
{ label: 'Stadium' },
{ label: 'Status' },
{ label: 'Resume' },
{ label: '' }]],
keys: [
'date',
'number',
'examinationDate',
'localization',
'stadium',
'resume',
'',
],
delKeyNames: [
{ key: 'date', label: 'Tanggal' },
{ key: 'number', label: 'Nomor' },
],
parses: {
parent: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
props: {
size: 'sm',
},
}
return res
},
},
htmls: {},
}
+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
// Components
import DataTable from '~/components/pub/my-ui/data-table/data-table.vue'
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list.cfg'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<DataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -0,0 +1,699 @@
<script setup lang="ts">
// Components
import * as DE from '~/components/pub/my-ui/doc-entry'
import { Button } from '~/components/pub/ui/button'
import { Input } from '~/components/pub/ui/input'
import * as CB from '~/components/pub/my-ui/combobox'
import DatepickerSingle from '~/components/pub/my-ui/datepicker/datepicker-single.vue'
import FileUpload from '~/components/pub/my-ui/form/file-field.vue'
// Types
import { IntegrationEncounterSchema, type IntegrationEncounterFormData } from '~/schemas/integration-encounter.schema'
import type { PatientEntity } from '~/models/patient'
// Helpers
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { refDebounced } from '@vueuse/core'
import type { Doctor } from '~/models/doctor'
// References
import { paymentMethodCodes } from '~/const/key-val/common'
// App things
import { genEncounter, type Encounter } from '~/models/encounter'
// Props
const props = defineProps<{
mode?: string
isLoading?: boolean
isReadonly?: boolean
isSepValid?: boolean
isMemberValid?: boolean
isCheckingSep?: boolean
doctorItems?: CB.Item[]
selectedDoctor: Doctor
// subSpecialist?: any[]
// specialists?: TreeItem[]
payments?: any[]
participantGroups?: any[]
seps: any[]
patient?: PatientEntity | null | undefined
objects?: any
}>()
// Model
const model = defineModel<Encounter>()
model.value = genEncounter()
// Common preparation
const defaultCBItems = [{ label: 'Pilih', value: '' }]
const paymentMethodItems = ref<any>({})
// Emit preparation
const emit = defineEmits<{
(e: 'onSelectDoctor', code: string): void
(e: 'event', menu: string, value?: any): void
(e: 'fetch', value?: any): void
}>()
// Validation schema
const { handleSubmit, errors, defineField, meta } = useForm<IntegrationEncounterFormData>({
validationSchema: toTypedSchema(IntegrationEncounterSchema),
})
// Bind fields and extract attrs
const [doctorCode, doctorCodeAttrs] = defineField('doctor_code')
const [unitCode, unitCodeAttrs] = defineField('unit_code')
const [registerDate, registerDateAttrs] = defineField('registerDate')
const [paymentMethodCode, paymentMethodCodeAttrs] = defineField('paymentMethod_code')
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 [sepFile, sepFileAttrs] = defineField('sepFile')
const [sippFile, sippFileAttrs] = defineField('sippFile')
const patientId = ref('')
const sepReference = ref('')
const sepControlDate = ref('')
const sepTrafficStatus = ref('')
const diagnosis = ref('')
const noteReference = ref('Hanya diperlukan jika pembayaran jenis JKN')
const noteFile = ref('Gunakan file [.pdf, .jpg, .png] dengan ukuran maksimal 1MB')
const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const mode = props.mode !== undefined ? props.mode : 'add'
// SEP validation state from props
const isSepValid = computed(() => props.isSepValid || false)
const isCheckingSep = computed(() => props.isCheckingSep || false)
const isInsurancePayment = computed(() => ['insurance', 'jkn'].includes(paymentMethodCode.value))
const isDateLoading = ref(false)
const debouncedSepNumber = refDebounced(sepNumber, 500)
const debouncedCardNumber = refDebounced(cardNumber, 500)
const sepFileReview = ref<any>(null)
const sippFileReview = ref<any>(null)
const unitFullName = ref('') // Unit, specialist, subspecialist
const formRef = ref<HTMLFormElement | null>(null) // Expose submit method for parent component
if (mode === 'add') {
// Set default sepDate to current date in YYYY-MM-DD format
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
registerDate.value = `${year}-${month}-${day}`
}
watch(
() => props.selectedDoctor,
(doctor) => {
unitFullName.value = doctor.subspecialist?.name ?? doctor.specialist?.name ?? doctor.unit?.name ?? 'tidak diketahui'
model.value!.unit_code = doctor.unit_code || ''
model.value!.specialist_code = doctor.specialist_code || ''
model.value!.subspecialist_code = doctor.subspecialist_code || ''
},
)
// 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 || ''
doctorCode.value = objects?.doctorCode || ''
paymentMethodCode.value = objects?.paymentMethodCode || ''
patientCategory.value = objects?.patientCategory || ''
cardNumber.value = objects?.cardNumber || ''
sepType.value = objects?.sepType || ''
sepNumber.value = objects?.sepNumber || ''
sepFileReview.value = objects?.sepFileReview || ''
sippFileReview.value = objects?.sippFileReview || ''
isDateLoading.value = true
setTimeout(() => {
registerDate.value = objects?.registerDate || ''
isDateLoading.value = false
}, 100)
}
},
{ deep: true, immediate: true },
)
watch(
() => props.patient,
(patient) => {
if (patient && Object.keys(patient).length > 0) {
patientId.value = patient?.id ? String(patient.id) : ''
patientName.value = patient?.person?.name || ''
nationalIdentity.value = patient?.person?.residentIdentityNumber || ''
medicalRecordNumber.value = patient?.number || ''
}
},
{ deep: true, immediate: true },
)
watch(
() => props.isSepValid,
(value) => {
if (!value) return
const objects = props.objects
if (objects && Object.keys(objects).length > 0) {
sepReference.value = objects?.sepReference || ''
sepControlDate.value = objects?.sepControlDate || ''
sepTrafficStatus.value = objects?.sepTrafficStatus || ''
diagnosis.value = objects?.diagnosis || ''
}
},
)
watch(debouncedSepNumber, (newValue) => {
emit('event', 'sep-number-changed', newValue)
})
watch(debouncedCardNumber, (newValue) => {
emit('event', 'member-changed', newValue)
})
function onAddSep() {
const formValues = {
patientId: patientId.value || '',
doctorCode: doctorCode.value,
registerDate: registerDate.value,
cardNumber: cardNumber.value,
paymentMethodCode: paymentMethodCode.value,
sepFile: sepFile.value,
sippFile: sippFile.value,
sepType: sepType.value,
}
emit('event', 'add-sep', formValues)
}
function onSearchSep() {
emit('event', 'search-sep', { cardNumber: cardNumber.value })
}
// Submit handler
const onSubmit = handleSubmit((values) => {
let payload: any = values
if (props.mode === 'edit') {
payload = {
...payload,
sepFileReview: sepFileReview.value,
sippFileReview: sippFileReview.value,
}
}
emit('event', 'save', payload)
})
function openFile(path: string) {
window.open(path, '_blank')
}
function submitForm() {
// Trigger form submit using native form submit
// This will trigger validation and onSubmit handler
if (formRef.value) {
formRef.value.requestSubmit()
} else {
// Fallback: directly call onSubmit handler
// Create a mock event object
const mockEvent = {
preventDefault: () => {},
target: formRef.value || {},
} as SubmitEvent
// Call onSubmit directly
onSubmit(mockEvent)
}
}
defineExpose({
submitForm,
})
onMounted(() => {
const isPaymentMethodVclaim = true
paymentMethodItems.value = isPaymentMethodVclaim ? props.payments : CB.recStrToItem(paymentMethodCodes)
})
</script>
<template>
<div class="mx-auto w-full">
<form
ref="formRef"
@submit.prevent="onSubmit"
class="grid gap-6 p-4"
>
<!-- Data Pasien -->
<div class="flex flex-col gap-2">
<h3 class="text-lg font-semibold">Data Pasien</h3>
<div class="flex items-center gap-2">
<span class="text-sm">sudah pernah terdaftar sebagai pasien?</span>
<Button
variant="outline"
type="button"
class="h-[40px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50"
@click="emit('event', 'search')"
>
<Icon
name="i-lucide-search"
class="h-5 w-5"
/>
Cari Pasien
</Button>
<span class="text-sm">belum pernah terdaftar sebagai pasien?</span>
<Button
variant="outline"
type="button"
class="h-[40px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50"
@click="emit('event', 'add')"
>
<Icon
name="i-lucide-plus"
class="h-5 w-5"
/>
Tambah Pasien Baru
</Button>
</div>
</div>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<DE.Label height="compact">Nama Pasien</DE.Label>
<DE.Field :errMessage="errors.patientName">
<Input
id="patientName"
v-model="patientName"
v-bind="patientNameAttrs"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">NIK</DE.Label>
<DE.Field :errMessage="errors.nationalIdentity">
<Input
id="nationalIdentity"
v-model="nationalIdentity"
v-bind="nationalIdentityAttrs"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">No. RM</DE.Label>
<DE.Field :errMessage="errors.medicalRecordNumber">
<Input
id="medicalRecordNumber"
v-model="medicalRecordNumber"
v-bind="medicalRecordNumberAttrs"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
<hr />
<!-- Data Kunjungan -->
<h3 class="text-lg font-semibold">Data Kunjungan</h3>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<DE.Label height="compact">
Dokter
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.doctor_code">
<CB.Combobox
id="doctorCode"
v-model="doctorCode"
v-bind="doctorCodeAttrs"
:items="[...defaultCBItems, ...doctorItems]"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih Dokter"
search-placeholder="Cari Dokter"
empty-message="Dokter tidak ditemukan"
@update:model-value="(value: any) => emit('onSelectDoctor', value)"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">
Spesialis / Subspesialis
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.unit_code">
<Input
:value="unitFullName"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">
Tanggal Daftar
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.registerDate">
<DatepickerSingle
v-if="!isDateLoading"
id="registerDate"
v-model="registerDate"
v-bind="registerDateAttrs"
placeholder="Pilih tanggal"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<DE.Label height="compact">
Jenis Pembayaran
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.paymentMethod_code">
<CB.Combobox
id="paymentMethodCode"
v-model="paymentMethodCode"
v-bind="paymentMethodCodeAttrs"
:items="paymentMethodItems"
:disabled="isLoading || isReadonly"
placeholder="Pilih Jenis Pembayaran"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
<!-- BPJS Fields (conditional) -->
<template v-if="isInsurancePayment">
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<DE.Label height="compact">
Kelompok Peserta
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.patientCategory">
<Select
id="patientCategory"
v-model="patientCategory"
v-bind="patientCategoryAttrs"
:items="participantGroups || []"
:disabled="isLoading || isReadonly"
placeholder="Pilih Kelompok Peserta"
/>
</DE.Field>
<span class="text-sm text-gray-500">
{{ noteReference }}
</span>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">
No. Kartu BPJS
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.cardNumber">
<Input
id="cardNumber"
v-model="cardNumber"
v-bind="cardNumberAttrs"
:disabled="isLoading || isReadonly"
placeholder="Masukkan nomor kartu BPJS"
/>
</DE.Field>
<div
v-if="isMemberValid"
class="mt-1 flex items-center gap-2"
>
<Icon
name="i-lucide-badge-check"
class="h-4 w-4 bg-green-500 text-white"
/>
<span class="text-sm text-green-500">Aktif</span>
</div>
<div
v-if="!isMemberValid"
class="mt-1 flex items-center gap-2"
>
<Icon
name="i-lucide-x"
class="h-4 w-4 bg-red-500 text-white"
/>
<span class="text-sm text-red-500">Tidak aktif</span>
</div>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">
Jenis SEP
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.sepType">
<Select
id="sepType"
v-model="sepType"
v-bind="sepTypeAttrs"
:items="seps"
:disabled="isLoading || isReadonly"
placeholder="Pilih Jenis SEP"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<DE.Label height="compact">
No. SEP
<span class="text-red-500">*</span>
</DE.Label>
<DE.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 || isSepValid"
/>
<Button
v-if="!isSepValid"
variant="outline"
type="button"
class="bg-primary"
size="sm"
:disabled="isCheckingSep || isLoading || isReadonly"
@click="onAddSep"
>
<Icon
v-if="isCheckingSep"
name="i-lucide-loader-2"
class="h-4 w-4 animate-spin"
/>
<Icon
v-else
name="i-lucide-plus"
class="h-4 w-4"
/>
</Button>
<Button
v-if="isMemberValid"
variant="outline"
type="button"
class="bg-primary"
size="sm"
@click="onSearchSep"
>
<Icon
name="i-lucide-search"
class="h-4 w-4"
/>
</Button>
</div>
</DE.Field>
<div
v-if="isSepValid"
class="mt-1 flex items-center gap-2"
>
<Icon
name="i-lucide-badge-check"
class="h-4 w-4 bg-green-500 text-white"
/>
<span class="text-sm text-green-500">Aktif</span>
</div>
<span class="text-sm text-gray-500">
{{ noteReference }}
</span>
</DE.Cell>
<DE.Cell>
<FileUpload
field-name="sepFile"
label="Dokumen SEP"
placeholder="Pilih file"
:accept="['pdf', 'jpg', 'png']"
:max-size-mb="1"
v-model="sepFile"
v-bind="sepFileAttrs"
@file-selected="() => {}"
/>
<span class="mt-1 text-sm text-gray-500">
{{ noteFile }}
</span>
<p v-if="sepFileReview">
<a
class="mt-1 text-sm capitalize text-blue-500"
href="#"
@click="openFile(sepFileReview.filePath)"
>
{{ sepFileReview?.fileName }}
</a>
</p>
</DE.Cell>
<DE.Cell>
<FileUpload
field-name="sippFile"
label="Dokumen SIPP"
placeholder="Pilih file"
:accept="['pdf', 'jpg', 'png']"
:max-size-mb="1"
v-model="sippFile"
v-bind="sippFileAttrs"
@file-selected="() => {}"
/>
<span class="mt-1 text-sm text-gray-500">
{{ noteFile }}
</span>
<p v-if="sippFileReview">
<a
class="mt-1 text-sm capitalize text-blue-500"
href="#"
@click="openFile(sippFileReview.filePath)"
>
{{ sippFileReview?.fileName }}
</a>
</p>
</DE.Cell>
</DE.Block>
</template>
<template v-if="isSepValid">
<hr />
<!-- Data SEP -->
<h3 class="text-lg font-semibold">Data SEP</h3>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<Label height="compact">Dengan Rujukan / Surat Kontrol</Label>
<DE.Field>
<Input
id="sepReference"
v-model="sepReference"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<Label height="compact">No. Rujukan / Surat Kontrol</Label>
<DE.Field>
<Input
id="sepReference"
v-model="sepNumber"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<Label height="compact">
Tanggal Rujukan / Surat Kontrol
<span class="ml-1 text-red-500">*</span>
</Label>
<DE.Field>
<DatepickerSingle
id="sepControlDate"
v-model="sepControlDate"
:disabled="true"
placeholder="Pilih tanggal sep"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell :col-span="2">
<Label height="compact">Diagnosis</Label>
<DE.Field>
<Input
id="diagnosis"
v-model="diagnosis"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<Label height="compact">Status Kecelakaan</Label>
<DE.Field>
<Input
id="sepTrafficStatus"
v-model="sepTrafficStatus"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
</template>
</form>
</div>
</template>
+12 -13
View File
@@ -23,7 +23,6 @@ import { paymentMethodCodes } from '~/const/key-val/common'
// App things
import { genEncounter, type Encounter } from '~/models/encounter'
import { se } from 'date-fns/locale'
// Props
const props = defineProps<{
@@ -50,7 +49,6 @@ model.value = genEncounter()
// Common preparation
const defaultCBItems = [{ label: 'Pilih', value: '' }]
const paymentMethodItems = CB.recStrToItem(paymentMethodCodes)
// Emit preparation
const emit = defineEmits<{
@@ -85,12 +83,10 @@ const sepTrafficStatus = ref('')
const diagnosis = ref('')
const noteReference = ref('Hanya diperlukan jika pembayaran jenis JKN')
const noteFile = ref('Gunakan file [.pdf, .jpg, .png] dengan ukuran maksimal 1MB')
const mode = props.mode !== undefined ? props.mode : 'add'
const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const mode = props.mode !== undefined ? props.mode : 'add'
// SEP validation state from props
const isSepValid = computed(() => props.isSepValid || false)
const isSepValid = computed(() => props.isSepValid || false) // SEP validation state from props
const isCheckingSep = computed(() => props.isCheckingSep || false)
const isInsurancePayment = computed(() => ['insurance', 'jkn'].includes(paymentMethodCode.value))
const isDateLoading = ref(false)
@@ -100,6 +96,7 @@ const sepFileReview = ref<any>(null)
const sippFileReview = ref<any>(null)
const unitFullName = ref('') // Unit, specialist, subspecialist
const formRef = ref<HTMLFormElement | null>(null) // Expose submit method for parent component
const paymentMethodItems = CB.recStrToItem(paymentMethodCodes)
if (mode === 'add') {
// Set default sepDate to current date in YYYY-MM-DD format
@@ -129,13 +126,15 @@ watch(
nationalIdentity.value = objects?.nationalIdentity || ''
medicalRecordNumber.value = objects?.medicalRecordNumber || ''
doctorCode.value = objects?.doctorCode || ''
paymentMethodCode.value = objects?.paymentMethodCode || ''
patientCategory.value = objects?.patientCategory || ''
cardNumber.value = objects?.cardNumber || ''
sepType.value = objects?.sepType || ''
sepNumber.value = objects?.sepNumber || ''
sepFileReview.value = objects?.sepFileReview || ''
sippFileReview.value = objects?.sippFileReview || ''
if (objects.paymentType) {
paymentMethodCode.value = objects.paymentType || ''
}
isDateLoading.value = true
setTimeout(() => {
registerDate.value = objects?.registerDate || ''
@@ -348,7 +347,7 @@ defineExpose({
placeholder="Pilih Dokter"
search-placeholder="Cari Dokter"
empty-message="Dokter tidak ditemukan"
@update:model-value="(value) => emit('onSelectDoctor', value)"
@update:model-value="(value: any) => emit('onSelectDoctor', value)"
/>
</DE.Field>
</DE.Cell>
@@ -395,12 +394,12 @@ defineExpose({
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.paymentMethod_code">
<CB.Combobox
<Select
id="paymentMethodCode"
v-model="paymentMethodCode"
v-bind="paymentMethodCodeAttrs"
:items="paymentMethodItems"
:disabled="isLoading || isReadonly"
:items="payments || []"
:disabled="isLoading || isReadonly || mode === 'edit'"
placeholder="Pilih Jenis Pembayaran"
/>
</DE.Field>
@@ -576,7 +575,7 @@ defineExpose({
</span>
<p v-if="sepFileReview">
<a
class="mt-1 text-sm text-blue-500 capitalize"
class="mt-1 text-sm capitalize text-blue-500"
href="#"
@click="openFile(sepFileReview.filePath)"
>
@@ -601,7 +600,7 @@ defineExpose({
</span>
<p v-if="sippFileReview">
<a
class="mt-1 text-sm text-blue-500 capitalize"
class="mt-1 text-sm capitalize text-blue-500"
href="#"
@click="openFile(sippFileReview.filePath)"
>
+129 -39
View File
@@ -1,50 +1,140 @@
<script setup lang="ts">
import Block from '~/components/pub/my-ui/form/block.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'
// 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/Button.vue'
const props = defineProps<{ modelValue: any }>()
const emit = defineEmits(['update:modelValue', 'event'])
// Types
import type { ItemPriceFormData } from '~/schemas/item-price.schema'
const data = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
// Helpers
import type z from 'zod'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
interface Props {
schema: z.ZodSchema<any>
items: any[]
values: any
isLoading?: boolean
isReadonly?: boolean
}
const props = defineProps<Props>()
const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const emit = defineEmits<{
submit: [values: ItemPriceFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
item_code: '',
price: 0,
insuranceCompany_code: '',
} as Partial<ItemPriceFormData>,
})
const items = [
{ value: '1', label: 'item 1' },
{ value: '2', label: 'item 2' },
{ value: '3', label: 'item 3' },
{ value: '4', label: 'item 4' },
]
const [item_code, item_codeAttrs] = defineField('item_code')
const [price, priceAttrs] = defineField('price')
const [insuranceCompany_code, insuranceCompany_codeAttrs] = defineField('insuranceCompany_code')
if (props.values) {
if (props.values.item_code !== undefined) item_code.value = props.values.item_code
if (props.values.price !== undefined) price.value = props.values.price
if (props.values.insuranceCompany_code !== undefined) insuranceCompany_code.value = props.values.insuranceCompany_code
}
const resetForm = () => {
item_code.value = ''
price.value = 0
insuranceCompany_code.value = ''
}
function onSubmitForm() {
const formData: ItemPriceFormData = {
item_code: item_code.value || '',
price: Number(price.value) || 0,
insuranceCompany_code: insuranceCompany_code.value || '',
}
emit('submit', formData, resetForm)
}
function onCancelForm() {
emit('cancel', resetForm)
}
</script>
<template>
<form id="entry-form">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<Block>
<FieldGroup>
<Label>Items</Label>
<Field>
<Select :items="items" />
</Field>
</FieldGroup>
<FieldGroup>
<Label>Perusahaan Insuransi</Label>
<Field>
<Select :items="items" />
</Field>
</FieldGroup>
<FieldGroup>
<Label>Harga</Label>
<Field>
<Input v-model="data.price" />
</Field>
</FieldGroup>
</Block>
</div>
<form
id="form-item-price"
@submit.prevent
>
<Block
labelSize="thin"
class="!mb-2.5 !pt-0 xl:!mb-3"
:colCount="1"
>
<Cell>
<Label height="compact">Item</Label>
<Field :errMessage="errors.item_code">
<Select
id="item_code"
v-model="item_code"
icon-name="i-lucide-chevron-down"
placeholder="Pilih Item"
v-bind="item_codeAttrs"
:items="items"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Harga</Label>
<Field :errMessage="errors.price">
<Input
id="price"
type="number"
v-model="price"
v-bind="priceAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Perusahaan Asuransi</Label>
<Field :errMessage="errors.insuranceCompany_code">
<Input
id="insuranceCompany_code"
v-model="insuranceCompany_code"
v-bind="insuranceCompany_codeAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
</Block>
<div class="my-2 flex justify-end gap-2 py-2">
<Button
type="button"
variant="secondary"
class="w-[120px]"
@click="onCancelForm"
>
Kembali
</Button>
<Button
v-if="!isReadonly"
type="button"
class="w-[120px]"
:disabled="isLoading || !meta.valid"
@click="onSubmitForm"
>
Simpan
</Button>
</div>
</form>
</template>
+8 -16
View File
@@ -3,27 +3,23 @@ import { defineAsyncComponent } from 'vue'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
const _doctorStatus = {
0: 'Tidak Aktif',
1: 'Aktif',
}
export const config: Config = {
cols: [{}, {}, { width: 50 }],
cols: [{}, {}, {}, { width: 50 }],
headers: [
[
{ label: 'Kode' },
{ label: 'Nama' },
{ label: 'Item' },
{ label: 'Harga' },
{ label: 'Perusahaan Asuransi' },
{ label: 'Aksi' },
],
],
keys: ['code', 'name', 'action'],
keys: ['item_code', 'price', 'insuranceCompany_code', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
{ key: 'item_code', label: 'Item' },
{ key: 'insuranceCompany_code', label: 'Perusahaan Asuransi' },
],
parses: {},
@@ -39,9 +35,5 @@ export const config: Config = {
},
},
htmls: {
patient_address(_rec) {
return '-'
},
},
htmls: {},
}
+30 -5
View File
@@ -1,14 +1,39 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list-cfg'
defineProps<{
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<PubMyUiDataTable
v-bind="config"
:rows="data"
/>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
</template>
@@ -1,29 +0,0 @@
<script setup lang="ts">
import { Badge } from '~/components/pub/ui/badge'
const props = defineProps<{
rec: any
idx?: number
}>()
const doctorStatus = {
0: 'Tidak Aktif',
1: 'Aktif',
}
const statusText = computed(() => {
return doctorStatus[props.rec.status_code as keyof typeof doctorStatus]
})
const badgeVariant = computed(() => {
return props.rec.status_code === 1 ? 'default' : 'destructive'
})
</script>
<template>
<div class="flex justify-center">
<Badge :variant="badgeVariant">
{{ statusText }}
</Badge>
</div>
</template>
+216 -57
View File
@@ -1,68 +1,227 @@
<script setup lang="ts">
import Block from '~/components/pub/my-ui/form/block.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'
// 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/Button.vue'
const props = defineProps<{ modelValue: any }>()
const emit = defineEmits(['update:modelValue', 'event'])
// Types
import type { ItemFormData } from '~/schemas/item.schema'
const data = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
// Helpers
import type z from 'zod'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
interface Props {
schema: z.ZodSchema<any>
itemGroups: any[]
uoms: any[]
values: any
isLoading?: boolean
isReadonly?: boolean
}
const props = defineProps<Props>()
const isShowInfra = false;
const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const emit = defineEmits<{
submit: [values: ItemFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
code: '',
name: '',
itemGroup_code: '',
uom_code: '',
infra_code: '',
stock: 0,
buyingPrice: 0,
sellingPrice: 0,
} as Partial<ItemFormData>,
})
const items = [
{ value: '1', label: 'item 1' },
{ value: '2', label: 'item 2' },
{ value: '3', label: 'item 3' },
{ value: '4', label: 'item 4' },
]
const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name')
const [itemGroup_code, itemGroup_codeAttrs] = defineField('itemGroup_code')
const [uom, uomAttrs] = defineField('uom_code')
const [infra_code, infra_codeAttrs] = defineField('infra_code')
const [stock, stockAttrs] = defineField('stock')
const [buyingPrice, buyingPriceAttrs] = defineField('buyingPrice')
const [sellingPrice, sellingPriceAttrs] = defineField('sellingPrice')
if (props.values) {
if (props.values.code !== undefined) code.value = props.values.code
if (props.values.name !== undefined) name.value = props.values.name
if (props.values.itemGroup_code !== undefined) itemGroup_code.value = props.values.itemGroup_code
if (props.values.uom_code !== undefined) uom.value = props.values.uom_code
if (props.values.infra_code !== undefined) infra_code.value = props.values.infra_code
if (props.values.stock !== undefined) stock.value = props.values.stock
if (props.values.buyingPrice !== undefined) buyingPrice.value = props.values.buyingPrice
if (props.values.sellingPrice !== undefined) sellingPrice.value = props.values.sellingPrice
}
const resetForm = () => {
code.value = ''
name.value = ''
itemGroup_code.value = ''
uom.value = ''
infra_code.value = ''
stock.value = 0
buyingPrice.value = 0
sellingPrice.value = 0
}
function onSubmitForm() {
const formData: ItemFormData = {
code: code.value || '',
name: name.value || '',
itemGroup_code: itemGroup_code.value || '',
uom_code: uom.value || '',
infra_code: infra_code.value || '',
stock: Number(stock.value) || 0,
buyingPrice: Number(buyingPrice.value) || 0,
sellingPrice: Number(sellingPrice.value) || 0,
}
emit('submit', formData, resetForm)
}
function onCancelForm() {
emit('cancel', resetForm)
}
</script>
<template>
<form id="entry-form">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<Block>
<FieldGroup>
<Label>Nama</Label>
<Field>
<Input v-model="data.name" />
</Field>
</FieldGroup>
<FieldGroup>
<Label>Kode</Label>
<Field>
<Input v-model="data.code" />
</Field>
</FieldGroup>
<FieldGroup>
<Label>Item Group</Label>
<Field>
<Select :items="items" />
</Field>
</FieldGroup>
<FieldGroup>
<Label>UOM</Label>
<Field>
<Select :items="items" />
</Field>
</FieldGroup>
<FieldGroup>
<Label>Infra</Label>
<Field>
<Select :items="items" />
</Field>
</FieldGroup>
<FieldGroup>
<Label>Harga</Label>
<Field>
<Input v-model="data.price" />
</Field>
</FieldGroup>
</Block>
</div>
<form
id="form-item"
@submit.prevent
>
<Block
labelSize="thin"
class="!mb-2.5 !pt-0 xl:!mb-3"
:colCount="1"
>
<Cell>
<Label height="compact">Kode</Label>
<Field :errMessage="errors.code">
<Input
id="code"
v-model="code"
v-bind="codeAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Nama</Label>
<Field :errMessage="errors.name">
<Input
id="name"
v-model="name"
v-bind="nameAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Item Group</Label>
<Field :errMessage="errors.itemGroup_code">
<Select
id="itemGroup_code"
v-model="itemGroup_code"
icon-name="i-lucide-chevron-down"
placeholder="Pilih Item Group"
v-bind="itemGroup_codeAttrs"
:items="itemGroups"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">UOM</Label>
<Field :errMessage="errors.uom_code">
<Select
id="uom_code"
v-model="uom"
icon-name="i-lucide-chevron-down"
placeholder="Pilih satuan"
v-bind="uomAttrs"
:items="uoms"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell v-if="isShowInfra">
<Label height="compact">Infra</Label>
<Field :errMessage="errors.infra_code">
<Input
id="infra_code"
v-model="infra_code"
v-bind="infra_codeAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Stok</Label>
<Field :errMessage="errors.stock">
<Input
id="stock"
type="number"
v-model="stock"
v-bind="stockAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Harga Beli</Label>
<Field :errMessage="errors.buyingPrice">
<Input
id="buyingPrice"
type="number"
v-model="buyingPrice"
v-bind="buyingPriceAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Harga Jual</Label>
<Field :errMessage="errors.sellingPrice">
<Input
id="sellingPrice"
type="number"
v-model="sellingPrice"
v-bind="sellingPriceAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
</Block>
<div class="my-2 flex justify-end gap-2 py-2">
<Button
type="button"
variant="secondary"
class="w-[120px]"
@click="onCancelForm"
>
Kembali
</Button>
<Button
v-if="!isReadonly"
type="button"
class="w-[120px]"
:disabled="isLoading || !meta.valid"
@click="onSubmitForm"
>
Simpan
</Button>
</div>
</form>
</template>
+57 -13
View File
@@ -3,30 +3,78 @@ import { defineAsyncComponent } from 'vue'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
const _doctorStatus = {
0: 'Tidak Aktif',
1: 'Aktif',
}
export const config: Config = {
cols: [{}, {}, { width: 50 }],
cols: [
{ width: 100 },
{ width: 100 },
{ width: 100 },
{ width: 100 },
{ width: 100 },
{ width: 100 },
{ width: 100 },
{ width: 100 },
{ width: 50 },
],
headers: [
[
{ label: 'Kode' },
{ label: 'Nama' },
{ label: 'Item Group' },
{ label: 'UOM' },
{ label: 'Infra' },
{ label: 'Stok' },
{ label: 'Harga Beli' },
{ label: 'Harga Jual' },
{ label: 'Aksi' },
],
],
keys: ['code', 'name', 'action'],
keys: ['code', 'name', 'itemGroup_code', 'uom_code', 'infra_code', 'stock', 'buyingPrice', 'sellingPrice', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {},
parses: {
itemGroup_code: (rec: unknown): unknown => {
const recX = rec as any
return recX.itemGroup_code || '-'
},
uom_code: (rec: unknown): unknown => {
const recX = rec as any
return recX.uom?.name || '-'
},
infra_code: (rec: unknown): unknown => {
const recX = rec as any
return recX.infra_code || '-'
},
stock: (rec: unknown): unknown => {
const recX = rec as any
const value = recX.stock
if (value === null || value === undefined) {
return '-'
}
return value
},
buyingPrice: (rec: unknown): unknown => {
const recX = rec as any
const value = recX.buyingPrice
if (value === null || value === undefined) {
return '-'
}
return value
},
sellingPrice: (rec: unknown): unknown => {
const recX = rec as any
const value = recX.sellingPrice
if (value === null || value === undefined) {
return '-'
}
return value
},
},
components: {
action(rec, idx) {
@@ -39,9 +87,5 @@ export const config: Config = {
},
},
htmls: {
patient_address(_rec) {
return '-'
},
},
htmls: {},
}
+30 -5
View File
@@ -1,14 +1,39 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list-cfg'
defineProps<{
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<PubMyUiDataTable
v-bind="config"
:rows="data"
/>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
</template>
View File
View File
-29
View File
@@ -1,29 +0,0 @@
<script setup lang="ts">
import { Badge } from '~/components/pub/ui/badge'
const props = defineProps<{
rec: any
idx?: number
}>()
const doctorStatus = {
0: 'Tidak Aktif',
1: 'Aktif',
}
const statusText = computed(() => {
return doctorStatus[props.rec.status_code as keyof typeof doctorStatus]
})
const badgeVariant = computed(() => {
return props.rec.status_code === 1 ? 'default' : 'destructive'
})
</script>
<template>
<div class="flex justify-center">
<Badge :variant="badgeVariant">
{{ statusText }}
</Badge>
</div>
</template>
@@ -0,0 +1,157 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
import * as Cbx from '~/components/pub/my-ui/checkboxes'
import { fi } from 'date-fns/locale';
const substances = [
{ value: 'biopsi', label: 'Biopsi'},
{ value: 'operation', label: 'Operasi' },
{ value: 'scraping', label: 'Kerokan' },
{ value: 'cytology', label: 'Sitologi' },
{ value: 'fnab', label: 'FNAB' },
]
const fications = [
{ value: 'pa-formaline-10p', label: 'pA. Formalin 10%' },
{ value: 'spt-alcohol-70p', label: 'Sputum Alkohol 70%' },
{ value: 'urn-alcohol-50p', label: 'Urine Alkohol 50%' },
{ value: 'vgn-smr-alcohol-95p', label: 'Vagibal Smear 95%' },
]
const prevAp = [
{ value: 'clinical', label: 'Klinik' },
{ value: 'ro', label: 'RO' },
{ value: 'clinical-pat', label: 'Pat. Klinik' },
{ value: 'operation', label: 'Operasi' },
{ value: 'necropsy', label: 'Nekropsi' },
]
</script>
<template>
<div class="header">Data Order</div>
<DE.Block :col-count="3" :cell-flex="false">
<DE.Cell>
<DE.Label>Tgl. Order</DE.Label>
<DE.Field>
<Input readonly />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>DPJP</DE.Label>
<DE.Field>
<Input readonly />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>PPDS</DE.Label>
<DE.Field>
<Input readonly />
</DE.Field>
</DE.Cell>
</DE.Block>
<div class="header">Bahan</div>
<DE.Block :cell-flex="false">
<DE.Cell>
<DE.Field>
<Cbx.Checkboxes :items="substances" :use-flex="true" />
</DE.Field>
</DE.Cell>
</DE.Block>
<div class="header">Fiksasi</div>
<DE.Block :cell-flex="false">
<DE.Cell>
<DE.Field>
<Cbx.Checkboxes :items="fications" :use-flex="true" />
</DE.Field>
</DE.Cell>
</DE.Block>
<div class="header">Data Klinis Pasien</div>
<DE.Block :col-count="6" :cell-flex="false">
<DE.Cell :col-span="3">
<DE.Label>Lokalisasi</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
<DE.Cell :col-span="3">
<DE.Label>Diagnosa Klinik</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Stadium T</DE.Label>
<DE.Field>
<Input />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Stadium M</DE.Label>
<DE.Field>
<Input />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Stadium N</DE.Label>
<DE.Field>
<Input />
</DE.Field>
</DE.Cell>
<div class=""></div>
<DE.Cell :col-span="3">
<DE.Label>Keterangan Klinik</DE.Label>
<DE.Field>
<Textarea rows="2" />
</DE.Field>
</DE.Cell>
</DE.Block>
<div class="header">Riwayat Pemeriksaan</div>
<DE.Block :col-count="2" :cell-flex="false">
<DE.Cell>
<DE.Label>Riwayat Dulu</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Riwayat Sekarang</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Pemeriksaan PA Sebelumnya</DE.Label>
<DE.Field>
<Cbx.Checkboxes :items="prevAp" :use-flex="true" />
</DE.Field>
</DE.Cell>
<div></div>
<DE.Cell>
<DE.Label>Keterangan PA Sebelumnya</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Pemeriksaan Penunjang</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
</DE.Block>
</template>
<style>
.header {
@apply mb-1.5 text-sm 2xl:text-base font-semibold
}
</style>
@@ -45,6 +45,7 @@ const {
handleSearch,
fetchData: getItemList,
} = usePaginatedList({
syncToUrl: false,
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
@@ -102,19 +103,23 @@ onMounted(async () => {
</script>
<template>
<Dialog v-model:open="isModalOpen" title="" size="xl">
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class=""
/>
<AppProcedureSrcList
:table-config="config"
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog
v-model:open="isModalOpen"
title=""
size="xl"
>
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class=""
/>
<AppProcedureSrcList
:table-config="config"
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</Dialog>
</template>
@@ -1,62 +1,143 @@
<script setup lang="ts">
import ProcedureListDialog from './procedure-list.vue'
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import { Form } from '~/components/pub/ui/form'
import { FieldArray } from 'vee-validate'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import * as DE from '~/components/pub/my-ui/doc-entry'
import TextAreaInput from '~/components/pub/my-ui/form/text-area-input.vue'
// Types
import { type ProcedureSrc } from '~/models/procedure-src'
// Helper
import { cn } from '~/lib/utils'
// Components
import { FieldArray } from 'vee-validate'
import { ButtonAction } from '~/components/pub/my-ui/form'
import TableHeader from '~/components/pub/ui/table/TableHeader.vue'
import { is } from 'date-fns/locale'
import ProcedureListDialog from './procedure-list.vue'
interface Props {
fieldName: string
title: string
subTitle?: string
// State UI (Loading / Disabled)
isReadonly?: boolean
// Data Architecture Switch
// 'form' = Pakai Vee-Validate (Parent wajib useForm)
// 'preview' = Pakai Props sampleItems (Parent bebas)
mode?: 'form' | 'preview'
// Data Source untuk mode 'preview' (atau initial data)
sampleItems?: ProcedureSrc[]
}
const props = defineProps<Props>()
// Set default mode ke 'form' agar backward compatible
const props = withDefaults(defineProps<Props>(), {
mode: 'form',
isReadonly: false,
sampleItems: () => [],
})
const isProcedurePickerDialogOpen = ref<boolean>(false)
provide(`isProcedurePickerDialogOpen`, isProcedurePickerDialogOpen)
</script>
<template>
<div class="">
<div class="mb-2 flex items-center justify-between">
<h1 class="mb-2 font-medium">{{ title }}</h1>
<Button @click="isProcedurePickerDialogOpen = true" size="xs" variant="outline"
class="text-orange-400 border-orange-400 bg-transparent">
<Icon name="i-lucide-search" class="h-4 w-4 align-middle transition-colors" />
Pilih Diagnosis
</Button>
<div class="mb-2 flex items-center justify-between">
<p class="mb-2 font-medium">{{ title }}</p>
<ButtonAction
v-if="mode === 'form' && !isReadonly"
preset="add"
title="Tambah Item"
icon="i-lucide-search"
:label="subTitle || 'Pilih Diagnosis'"
:full-width-mobile="true"
@click="isProcedurePickerDialogOpen = true"
/>
</div>
<FieldArray
v-if="mode === 'form'"
v-slot="{ fields, push, remove }"
:name="props.fieldName"
>
<ProcedureListDialog :process-fn="push" />
<div class="overflow-hidden rounded-lg border border-gray-200">
<Table>
<TableHeader class="bg-gray-100">
<TableRow>
<TableHead class="w-1/2">Prosedur</TableHead>
<TableHead class="w-1/2">ICD-X</TableHead>
<TableHead class="w-[50px]">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="fields.length === 0">
<TableCell
colspan="3"
class="py-4 text-center text-muted-foreground"
>
Belum ada data dipilih.
</TableCell>
</TableRow>
<TableRow
v-for="(field, idx) in fields"
:key="field.key"
>
<TableCell :class="cn(isReadonly && 'opacity-50')">
{{ (field.value as ProcedureSrc)?.name }}
</TableCell>
<TableCell :class="cn(isReadonly && 'opacity-50')">
{{ (field.value as ProcedureSrc)?.code }}
</TableCell>
<TableCell>
<ButtonAction
v-if="!isReadonly"
preset="delete"
icon-only
:title="`Hapus ${(field.value as ProcedureSrc)?.name}`"
@click="remove(idx)"
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</FieldArray>
<FieldArray v-slot="{ fields, push, remove }" :name="props.fieldName">
<ProcedureListDialog :process-fn="push" />
<div
v-else
class="overflow-hidden rounded-lg border border-gray-200"
>
<Table>
<TableHeader class="bg-gray-100">
<TableRow>
<TableHead class="w-1/2">Prosedur</TableHead>
<TableHead class="w-1/2">ICD-X</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="sampleItems.length === 0">
<TableCell
colspan="2"
class="py-4 text-center text-muted-foreground"
>
Tidak ada data.
</TableCell>
</TableRow>
<div class="border border-gray-200 rounded-lg overflow-hidden">
<Table>
<TableHeader class="bg-gray-100">
<TableRow>
<TableHead class="w-1/2">Prosedur</TableHead>
<TableHead class="w-1/2">ICD-X</TableHead>
<TableHead class="w-1/2">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="(field, idx) in fields" :key="idx">
<TableCell class="">{{ field.value?.name }}</TableCell>
<TableCell class="">{{ field.value?.code }}</TableCell>
<TableCell class="">
<Button type="button" variant="destructive" size="sm" @click="remove(idx)">
<Icon name="i-lucide-trash-2" class="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</FieldArray>
<TableRow
v-for="item in sampleItems"
:key="item.code || item.id"
>
<TableCell class="text-muted-foreground">
{{ item.name }}
</TableCell>
<TableCell class="text-muted-foreground">
{{ item.code }}
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</template>
@@ -0,0 +1,77 @@
<script setup lang="ts">
import Nav from '~/components/pub/my-ui/nav-footer/ba-de-su.vue'
import NavOk from '~/components/pub/my-ui/nav-footer/ok.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
import { type HeaderPrep } from '~/components/pub/my-ui/data/types'
// mcu src category
import ScrCategorySwitcher from '~/components/app/mcu-src-category/switcher.vue'
import { getList as getMcuCategoryList } from '~/services/mcu-src-category.service'
// mcu src
import { type McuSrc } from '~/models/mcu-src'
import { getList as getMcuSrcList } from '~/services/mcu-src.service'
import McuSrcPicker from '~/components/app/mcu-src/picker-accordion.vue'
// mcu order
import { getDetail } from '~/services/mcu-order.service'
import Detail from '~/components/app/mcu-order/detail.vue'
// mcu order item, manually not using composable
import {
getList as getMcuOrderItemList,
create as createMcuOrderItem,
remove as removeMcuOrderItem,
} from '~/services/mcu-order-item.service'
import { type McuOrderItem } from '~/models/mcu-order-item'
import Entry from '~/components/app/mcu-order/entry-for-ap.vue'
// props
const props = defineProps<{
encounter_id: number
}>()
// declaration & flows
// MCU Order
const { getQueryParam } = useQueryParam()
const id = getQueryParam('id')
const dataRes = await getDetail(
typeof id === 'string' ? parseInt(id) : 0,
{ includes: 'encounter,doctor,doctor-employee,doctor-employee-person' }
)
const data = dataRes.body?.data
// MCU Sources
const { backToList } = useQueryCRUDMode()
const headerPrep: HeaderPrep = {
title: 'Entry Order LAB PA',
icon: 'i-lucide-box',
}
function navClick(type: 'back' | 'delete' | 'draft' | 'submit') {
if (type === 'back') {
backToList()
}
}
</script>
<template>
<Header
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5"
/>
<Entry />
<Separator class="my-5" />
<div class="w-full flex justify-center">
<Nav @click="navClick" />
</div>
</template>
@@ -0,0 +1,159 @@
<script setup lang="ts">
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
handleActionSave,
handleActionRemove,
} from '~/handlers/mcu-order.handler'
// Apps
import { getList, getDetail } from '~/services/mcu-order.service'
import List from '~/components/app/mcu-order/list.vue'
import type { McuOrder } from '~/models/mcu-order'
const route = useRoute()
const { setQueryParams } = useQueryParam()
const { crudQueryParams } = useQueryCRUD()
const title = ref('')
const plainEid = route.params.id
const encounter_id = (plainEid && typeof plainEid == 'string') ? parseInt(plainEid) : 0 // here the
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getMyList,
} = usePaginatedList<McuOrder>({
fetchFn: async ({ page, search }) => {
const result = await getList({
search,
page,
'scope-code': "ap-lab",
'encounter-id': encounter_id,
includes: 'doctor,doctor-employee,doctor-employee-person',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'mcu-order'
})
const headerPrep: HeaderPrep = {
title: 'Order Lab PA',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (value: string) => {
searchInput.value = value
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
crudQueryParams.value = { mode: 'entry', recordId: undefined }
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getMyDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
// Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getMyDetail(recId.value)
title.value = 'Detail Order Lab PK'
isReadonly.value = true
break
case ActionEvents.showEdit:
getMyDetail(recId.value)
title.value = 'Edit Order Lab PK'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
watch([isFormEntryDialogOpen], async () => {
})
onMounted(async () => {
})
function cancel(data: McuOrder) {
recId.value = data.id
recItem.value = data
isRecordConfirmationOpen.value = true
}
function edit(data: McuOrder) {
setQueryParams({
'mode': 'entry',
'id': data.id.toString()
})
recItem.value = data
}
function submit(data: McuOrder) {
}
</script>
<template>
<Header :prep="{ ...headerPrep }" />
<List
v-if="!isLoading.dataListLoading"
:data="data"
:pagination-meta="paginationMeta"
@cancel="cancel"
@edit="edit"
@submit="submit"
/>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getMyList, toast)"
@cancel=""
/>
</template>
@@ -0,0 +1,16 @@
<script setup lang="ts">
//
import List from './list.vue'
import Entry from './entry.vue'
const props = defineProps<{
encounter_id: number
}>()
const { mode } = useQueryCRUDMode()
</script>
<template>
<List v-if="mode === 'list'" :encounter_id="encounter_id" />
<Entry v-else :encounter_id="encounter_id" />
</template>
@@ -188,6 +188,7 @@ onMounted(async () => {
@event="handleEvent"
@fetch="handleFetch"
/>
<AppViewPatient
v-model:open="openPatient"
v-model:selected="selectedPatient"
@@ -206,11 +207,13 @@ onMounted(async () => {
"
@save="handleSavePatient"
/>
<AppViewHistory
v-model:open="openHistory"
:is-action="true"
:histories="histories"
/>
<!-- Footer Actions -->
<div class="mt-6 flex justify-end gap-2 border-t border-t-slate-300 pt-4">
<Button
+9 -1
View File
@@ -29,6 +29,7 @@ import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
import Radiology from '~/components/content/radiology-order/main.vue'
import Consultation from '~/components/content/consultation/list.vue'
import Cprj from '~/components/content/cprj/entry.vue'
import ActionReport from '~/components/content/action-report/entry.vue'
import DocUploadList from '~/components/content/document-upload/list.vue'
import GeneralConsentList from '~/components/content/general-consent/entry.vue'
import SummaryMedic from '~/components/content/summary-medic/entry.vue'
@@ -57,7 +58,7 @@ const router = useRouter()
const { user, userActiveRole, getActiveRole } = useUserStore()
const activeRole = getActiveRole()
const activePosition = ref(getServicePosition(activeRole))
const menus = ref([] as any)
const menus = shallowRef([] as any)
const activeMenu = computed({
get: () => (route.query?.menu && typeof route.query.menu === 'string' ? route.query.menu : 'status'),
set: (value: string) => {
@@ -124,6 +125,13 @@ const protocolRows = [
{ value: 'resume', label: 'Resume', component: ResumeList, props: { encounter: data } },
{ value: 'control', label: 'Surat Kontrol', component: ControlLetterList, props: { encounter: data } },
{ value: 'screening', label: 'Skrinning MPP' },
{
value: 'report',
label: 'Laporan Tindakan',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: ActionReport,
props: { encounter: data },
},
{
value: 'supporting-document',
label: 'Upload Dokumen Pendukung',
+176 -48
View File
@@ -1,70 +1,198 @@
<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 Modal from '~/components/pub/my-ui/modal/modal.vue'
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import AppItemList from '~/components/app/item-price/list.vue'
import AppItemEntryForm from '~/components/app/item-price/entry-form.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
const data = ref([])
const entry = ref<any>({})
// Constants
import { infraGroupCodesKeys } from '~/lib/constants'
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (_val: string) => {
// filter patient list
},
onClear: () => {
// clear url param
},
}
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
// Loading state management
const isLoading = reactive<DataTableLoader>({
summary: false,
isTableLoading: false,
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { ItemPriceSchema, type ItemPriceFormData } from '~/schemas/item-price.schema'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/item-price.handler'
// Services
import { getList, getDetail } from '~/services/item-price.service'
import { getValueLabelList as getItemGroupList } from '~/services/item.service'
const items = ref<{ value: string | number; label: string }[]>([])
const title = ref('')
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getItemList,
} = usePaginatedList({
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10,
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'item-price',
})
const isOpen = ref(false)
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const hreaderPrep: HeaderPrep = {
title: 'Golongan Obat',
icon: 'i-lucide-users',
const headerPrep: HeaderPrep = {
title: 'Harga Item',
icon: 'i-lucide-layout-list',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (val: string) => {
searchInput.value = val
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
onClick: () => (isOpen.value = true),
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
async function getPatientList() {
isLoading.isTableLoading = true
const resp = await xfetch('/api/v1/medicine-group')
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
}
isLoading.isTableLoading = false
}
onMounted(() => {
getPatientList()
})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getCurrentDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
watch([recId, recAction], () => {
const currentId = recId.value
switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentDetail(currentId)
title.value = 'Detail Harga Item'
isReadonly.value = true
break
case ActionEvents.showEdit:
getCurrentDetail(currentId)
title.value = 'Edit Harga Item'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
onMounted(async () => {
items.value = await getItemGroupList()
await getItemList()
})
</script>
<template>
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
/>
<AppMedicineGroupList :data="data" />
<AppItemList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Modal v-model:open="isOpen" title="Tambah Golongan Obat" size="lg" prevent-outside>
<AppMedicineGroupEntryForm v-model="entry" />
</Modal>
<Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Harga Item'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppItemEntryForm
:schema="ItemPriceSchema"
:items="items"
:values="recItem"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: ItemPriceFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getItemList, resetForm, toast)
return
}
handleActionSave(values, getItemList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getItemList, 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>
+185 -47
View File
@@ -1,70 +1,208 @@
<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 Modal from '~/components/pub/my-ui/modal/modal.vue'
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import AppItemList from '~/components/app/item/list.vue'
import AppItemEntryForm from '~/components/app/item/entry-form.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
const data = ref([])
const entry = ref<any>({})
// Constants
import { itemGroupCodes } from '~/lib/constants'
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (_val: string) => {
// filter patient list
},
onClear: () => {
// clear url param
},
}
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
// Loading state management
const isLoading = reactive<DataTableLoader>({
summary: false,
isTableLoading: false,
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { ItemSchema, type ItemFormData } from '~/schemas/item.schema'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/item.handler'
// Services
import { getList, getDetail } from '~/services/item.service'
import { getValueLabelList as getUomList } from '~/services/uom.service'
const itemGroups = ref<{ value: string | number; label: string }[]>([])
const uoms = ref<{ value: string | number; label: string }[]>([])
const title = ref('')
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getItemList,
} = usePaginatedList({
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10,
includes: 'uom',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'item',
})
const isOpen = ref(false)
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const hreaderPrep: HeaderPrep = {
const headerPrep: HeaderPrep = {
title: 'Item',
icon: 'i-lucide-users',
icon: 'i-lucide-layout-list',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (val: string) => {
searchInput.value = val
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
onClick: () => (isOpen.value = true),
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
async function getPatientList() {
isLoading.isTableLoading = true
const resp = await xfetch('/api/v1/medicine-group')
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
}
isLoading.isTableLoading = false
}
onMounted(() => {
getPatientList()
})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getCurrentDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
watch([recId, recAction], () => {
const currentId = recItem.value?.code ? recItem.value.code : recId.value
switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentDetail(currentId)
title.value = 'Detail Item'
isReadonly.value = true
break
case ActionEvents.showEdit:
getCurrentDetail(currentId)
title.value = 'Edit Item'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
onMounted(async () => {
itemGroups.value = Object.keys(itemGroupCodes).map((key) => ({
value: key,
label: itemGroupCodes[key],
})) as any
uoms.value = await getUomList()
await getItemList()
})
</script>
<template>
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
/>
<AppItemList :data="data" />
<AppItemList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Modal v-model:open="isOpen" title="Tambah Golongan Obat" size="xl" prevent-outside>
<AppItemEntryForm v-model="entry" />
</Modal>
<Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Item'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppItemEntryForm
:schema="ItemSchema"
:values="recItem"
:item-groups="itemGroups"
:uoms="uoms"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: ItemFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recItem?.code ? recItem.code : recId, values, () => {
getItemList()
onResetState()
}, resetForm, toast)
return
}
handleActionSave(values, getItemList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recItem?.code ? recItem.code : recId, getItemList, 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>
+1 -1
View File
@@ -158,7 +158,7 @@ export const permissions: Record<string, Record<string, Permission[]>> = {
'emp|lab': ['R'],
'emp|rad': ['R'],
},
'/ambulatory/encounter/[id]/process?menu=pa-lab-order': {
'/ambulatory/encounter/[id]/process?menu=ap-lab-order': {
'emp|doc': ['R', 'U'],
'emp|nur': ['R'],
'emp|thr': ['R'],
+4 -3
View File
@@ -75,6 +75,7 @@ export function useEncounterEntry(props: {
const isSaveDisabled = computed(() => {
return !selectedPatient.value || !selectedPatientObject.value || isSaving.value || isLoadingDetail.value
})
const isUsePaymentNew = true
function getListPath(): string {
if (props.classCode === 'ambulatory') {
@@ -436,7 +437,7 @@ export function useEncounterEntry(props: {
formData.registerDate = date.toISOString().split('T')[0]
}
if (encounter.paymentMethod_code) {
if (!isUsePaymentNew && encounter.paymentMethod_code) {
formData.paymentMethodCode = encounter.paymentMethod_code
if (encounter.paymentMethod_code === 'insurance') {
formData.paymentType = 'jkn'
@@ -449,7 +450,7 @@ export function useEncounterEntry(props: {
}
}
} else {
formData.paymentType = 'spm'
formData.paymentType = encounter.paymentMethod_code
}
formData.cardNumber = encounter.member_number || ''
@@ -520,7 +521,7 @@ export function useEncounterEntry(props: {
sippFile.value = formValues.sippFile || null
let paymentMethodCode = formValues.paymentMethod_code ?? null
if (!paymentMethodCode) {
if (!isUsePaymentNew && !paymentMethodCode) {
if (formValues.paymentType === 'jkn' || formValues.paymentType === 'jkmm') {
paymentMethodCode = 'insurance'
} else if (formValues.paymentType === 'spm') {
+22 -6
View File
@@ -42,6 +42,7 @@ const PrescriptionAsync = defineAsyncComponent(() => import('~/components/conten
const CpLabOrderAsync = defineAsyncComponent(() => import('~/components/content/cp-lab-order/main.vue'))
const ProcedureRoomOrderAsync = defineAsyncComponent(() => import('~/components/content/procedure-room-order/main.vue'))
const MicroLabOrderAsync = defineAsyncComponent(() => import('~/components/content/micro-lab-order/main.vue'))
const ApLabOrderAsync = defineAsyncComponent(() => import('~/components/content/ap-lab-order/main.vue'))
const CprjAsync = defineAsyncComponent(() => import('~/components/content/cprj/entry.vue'))
const RadiologyAsync = defineAsyncComponent(() => import('~/components/content/radiology-order/main.vue'))
const ConsultationAsync = defineAsyncComponent(() => import('~/components/content/consultation/list.vue'))
@@ -57,6 +58,8 @@ const InitialNursingStudyAsync = defineAsyncComponent(() => import('~/components
const AssessmentEducationEntryAsync = defineAsyncComponent(
() => import('~/components/content/assessment-education/entry.vue'),
)
const SummaryMedicAsync = defineAsyncComponent(() => import('~/components/content/summary-medic/entry.vue'))
const ActionReportEntryAsync = defineAsyncComponent(() => import('~/components/content/action-report/entry.vue'))
const defaultKeys: Record<string, any> = {
status: {
@@ -166,8 +169,8 @@ const defaultKeys: Record<string, any> = {
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
paLabOrder: {
id: 'pa-lab-order',
apLabOrder: {
id: 'ap-lab-order',
title: 'Order Lab PA',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
@@ -388,10 +391,10 @@ export function injectComponents(id: string | number, data: EncounterListData, m
currentKeys.microLabOrder['component'] = MicroLabOrderAsync
currentKeys.microLabOrder['props'] = { encounter_id: id }
}
if (currentKeys?.paLabOrder) {
// TODO: add component for paLabOrder
currentKeys.paLabOrder['component'] = null
currentKeys.paLabOrder['props'] = { encounter_id: id }
if (currentKeys?.apLabOrder) {
// TODO: add component for apLabOrder
currentKeys.apLabOrder['component'] = ApLabOrderAsync
currentKeys.apLabOrder['props'] = { encounter_id: id }
}
if (currentKeys?.procedureRoomOrder) {
currentKeys.procedureRoomOrder['component'] = ProcedureRoomOrderAsync
@@ -455,6 +458,19 @@ export function injectComponents(id: string | number, data: EncounterListData, m
currentKeys.initialNursingStudy['props'] = { encounter: data?.encounter }
}
if (currentKeys?.initialNursingStudy) {
currentKeys.initialNursingStudy['component'] = InitialNursingStudyAsync
currentKeys.initialNursingStudy['props'] = { encounter: data?.encounter }
}
if (currentKeys?.actionReport) {
currentKeys.actionReport['component'] = ActionReportEntryAsync
currentKeys.actionReport['props'] = {
encounter: data?.encounter,
type: 'action-report',
label: currentKeys.actionReport['title'],
}
}
return currentKeys
}
+587
View File
@@ -0,0 +1,587 @@
import { isValidDate } from '~/lib/date'
import { medicalRoles } from '~/const/common/role'
export interface EncounterItem {
id: string
title: string
classCode?: string[]
unit?: string
afterId?: string
component?: any
props?: Record<string, any>
}
export interface EncounterProps {
classCode: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient'
subClassCode: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
}
export interface EncounterListData {
encounter?: any
status?: any
medicalAssessment?: any
medicalAssessmentRehab: any
functionAssessment?: any
protocolTheraphy?: any
protocolChemotherapy?: any
medicineProtocolChemotherapy?: any
consultation?: any
letterOfControl?: any
}
const StatusAsync = defineAsyncComponent(() => import('~/components/content/encounter/status.vue'))
const EarlyMedicalRehabListAsync = defineAsyncComponent(() => import('~/components/content/soapi/entry.vue'))
const EarlyMedicalAssesmentListAsync = defineAsyncComponent(() => import('~/components/content/soapi/entry.vue'))
const InitialNursingStudyAsync = defineAsyncComponent(() => import('~/components/content/initial-nursing/entry.vue'))
// const AssesmentFunctionListAsync = defineAsyncComponent(() => import('~/components/content/soapi/entry.vue'))
const KfrListAsync = defineAsyncComponent(() => import('~/components/content/kfr/main.vue'))
const ChemoProtocolListAsync = defineAsyncComponent(() => import('~/components/app/chemotherapy/list.protocol.vue'))
const ChemoMedicineProtocolListAsync = defineAsyncComponent(() => import('~/components/app/chemotherapy/list.medicine.vue'))
const GeneralConsentListAsync = defineAsyncComponent(() => import('~/components/content/general-consent/entry.vue'))
const CprjAsync = defineAsyncComponent(() => import('~/components/content/cprj/entry.vue'))
const PrescriptionAsync = defineAsyncComponent(() => import('~/components/content/prescription/main.vue'))
const DeviceOrderAsync = defineAsyncComponent(() => import('~/components/content/device-order/main.vue'))
const RadiologyOrderAsync = defineAsyncComponent(() => import('~/components/content/radiology-order/main.vue'))
const CpLabOrderAsync = defineAsyncComponent(() => import('~/components/content/cp-lab-order/main.vue'))
const MicroLabOrderAsync = defineAsyncComponent(() => import('~/components/content/micro-lab-order/main.vue'))
const ApLabOrderAsync = defineAsyncComponent(() => import('~/components/content/ap-lab-order/main.vue'))
const ProcedureRoomOrderAsync = defineAsyncComponent(() => import('~/components/content/procedure-room-order/main.vue'))
const ConsultationAsync = defineAsyncComponent(() => import('~/components/content/consultation/list.vue'))
const DocUploadListAsync = defineAsyncComponent(() => import('~/components/content/document-upload/main.vue'))
const ResumeListAsync = defineAsyncComponent(() => import('~/components/content/resume/main.vue'))
const ControlLetterListAsync = defineAsyncComponent(() => import('~/components/content/control-letter/main.vue'))
const PrbListAsync = defineAsyncComponent(() => import('~/components/content/prb/main.vue'))
const SurgeryReportListAsync = defineAsyncComponent(() => import('~/components/content/surgery-report/main.vue'))
const VaccineDataListAsync = defineAsyncComponent(() => import('~/components/content/vaccine-data/main.vue'))
const defaultKeys: Record<string, any> = {
status: {
id: 'status',
title: 'Status Masuk/Keluar',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
earlyMedicalRehabAssessment: {
id: 'rehab-medical-assessment',
title: 'Pengkajian Awal Medis Rehabilitasi Medis',
classCode: ['ambulatory'],
unit: 'rehab',
},
earlyMedicalAssessment: {
id: 'early-medical-assessment',
title: 'Pengkajian Awal Medis',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
afterId: 'early-medical-assessment',
},
initialNursingStudy: {
id: 'initial-nursing-study',
title: 'Kajian Awal Keperawatan',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
fkr: {
id: 'fkr',
title: 'FKR',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'rehab',
},
chemotherapyProtocol: {
id: 'chemotherapy-protocol',
title: 'Protokol Kemoterapi',
classCode: ['ambulatory'],
unit: 'chemo',
},
chemotherapyMedicine: {
id: 'chemotherapy-medicine',
title: 'Protokol Obat Kemoterapi',
classCode: ['ambulatory'],
unit: 'chemo',
},
educationAssessment: {
id: 'education-assessment',
title: 'Asesmen Kebutuhan Edukasi',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
// NOTE: Replaced by FRK
// functionAssessment: {
// id: 'function-assessment',
// title: 'Asesmen Fungsi',
// classCode: ['ambulatory'],
// unit: 'rehab',
// afterId: 'rehab-medical-assessment',
// },
// therapyProtocol: {
// id: 'therapy-protocol',
// classCode: ['ambulatory'],
// title: 'Protokol Terapi',
// unit: 'rehab',
// afterId: 'function-assessment',
// },
generalConsent: {
id: 'general-consent',
title: 'General Consent',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
patientAmbNote: { // CPRJ
id: 'patient-amb-note',
title: 'CPRJ',
classCode: ['ambulatory', 'emergency'],
unit: 'ambulatory',
},
patientDevNote: { // CPPT
id: 'patient-dev-note',
title: 'CPP',
classCode: ['inpatient'],
unit: 'inpatient',
},
prescription: {
id: 'prescription',
title: 'Order Obat',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
deviceOrder: {
id: 'device-order',
title: 'Order Alkes',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
radiologyOrder: {
id: 'radiology-order',
title: 'Order Radiologi',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
cpLabOrder: {
id: 'cp-lab-order',
title: 'Order Lab PK',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
microLabOrder: {
id: 'micro-lab-order',
title: 'Order Lab Mikro',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
paLabOrder: {
id: 'pa-lab-order',
title: 'Order Lab PA',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
procedureRoomOrder: {
id: 'procedure-room-order',
title: 'Order Ruang Tindakan',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
mcuResult: {
id: 'mcu-result',
title: 'Hasil Penunjang',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
procedureReport: {
id: 'action-report',
title: 'Laporan Tindakan',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
operationReport: {
id: 'action-report',
title: 'Laporan Tindakan',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
ambulanceOrder: {
id: 'ambulance-order',
title: 'Order Ambulans',
classCode: ['ambulatory', 'emergency'],
unit: 'all',
},
surgeryReport: {
id: 'surgery-report',
title: 'Laporan Operasi',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
forensic: {
id: 'surgery-report',
title: 'Laporan Operasi',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
consultation: {
id: 'consultation',
title: 'Konsultasi',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
resume: {
id: 'resume',
title: 'Resume Medis',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
controlLetter: {
id: 'control-letter',
title: 'Surat Kontrol',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
inpatientLetter: {
id: 'inpatient-letter',
title: 'SPRI',
classCode: ['ambulatory', 'emergency'],
unit: 'all',
},
refBack: {
id: 'reference-back',
title: 'PRB',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
screening: {
id: 'screening',
title: 'Skrinning MPP',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
supportingDocument: {
id: 'supporting-document',
title: 'Upload Dokumen Pendukung',
classCode: ['ambulatory'],
unit: 'all',
},
vaccineData: {
id: 'vaccine-data',
title: 'Data Vaksin',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
priceList: {
id: 'price-list',
title: 'Tarif Tindakan',
classCode: ['ambulatory', 'emergency', 'inpatient'],
unit: 'all',
},
}
export function getItemsByClassCode(classCode: string, items: EncounterItem[]) {
return items.filter((item) => item.classCode?.includes(classCode))
}
export function getItemsByUnit(unit: string, items: EncounterItem[]) {
return items.filter((item) => item.unit === unit)
}
export function getItemsByIds(ids: string[], items: EncounterItem[]) {
return items.filter((item) => ids.includes(item.id))
}
export function getIndexById(id: string, items: EncounterItem[]) {
return items.findIndex((item) => item.id === id)
}
export const getItemsAll = (classCode: string, unit: string, items: EncounterItem[]) => {
const prevItems = [...items]
let updateItems = getItemsByClassCode(classCode, prevItems)
updateItems = getItemsByUnit(unit, updateItems)
return updateItems
}
export function insertItemByAfterId(id: string, items: EncounterItem[], newItem: EncounterItem) {
const index = getIndexById(id, items)
if (index > -1) {
items.splice(index + 1, 0, newItem)
}
}
export function injectComponents(id: string | number, data: EncounterListData, meta: EncounterListData) {
const currentKeys = { ...defaultKeys }
if (currentKeys?.status) {
currentKeys.status['component'] = StatusAsync
currentKeys.status['props'] = { encounter: data?.encounter }
}
if (currentKeys?.earlyMedicalAssessment) {
currentKeys.earlyMedicalAssessment['component'] = EarlyMedicalAssesmentListAsync
currentKeys.earlyMedicalAssessment['props'] = {
encounter: data?.encounter,
type: 'early-medic',
label: currentKeys.earlyMedicalAssessment['title'],
}
}
if (currentKeys?.earlyMedicalRehabAssessment) {
currentKeys.earlyMedicalRehabAssessment['component'] = EarlyMedicalRehabListAsync
currentKeys.earlyMedicalRehabAssessment['props'] = {
encounter: data?.encounter,
type: 'early-rehab',
label: currentKeys.earlyMedicalRehabAssessment['title'],
}
}
if (currentKeys?.functionAssessment) {
currentKeys.functionAssessment['component'] = AssesmentFunctionListAsync
currentKeys.functionAssessment['props'] = {
encounter: data?.encounter,
type: 'function',
label: currentKeys.functionAssessment['title'],
}
}
if (currentKeys?.therapyProtocol) {
// TODO: add component for therapyProtocol
currentKeys.therapyProtocol['component'] = null
currentKeys.therapyProtocol['props'] = {
data: data?.encounter,
paginationMeta: meta?.protocolTheraphy,
}
}
if (currentKeys?.chemotherapyProtocol) {
currentKeys.chemotherapyProtocol['component'] = ChemoProtocolListAsync
currentKeys.chemotherapyProtocol['props'] = {
data: data?.encounter,
paginationMeta: meta?.protocolChemotherapy,
}
}
if (currentKeys?.chemotherapyMedicine) {
currentKeys.chemotherapyMedicine['component'] = ChemoMedicineProtocolListAsync
currentKeys.chemotherapyMedicine['props'] = {
data: data?.encounter,
paginationMeta: meta?.medicineProtocolChemotherapy,
}
}
if (currentKeys?.educationAssessment) {
// TODO: add component for education assessment
currentKeys.educationAssessment['component'] = null
currentKeys.educationAssessment['props'] = { encounter_id: id }
}
if (currentKeys?.generalConsent) {
currentKeys.generalConsent['component'] = GeneralConsentListAsync
currentKeys.generalConsent['props'] = { encounter_id: id }
}
if (currentKeys?.patientAmbNote) {
currentKeys.patientAmbNote['component'] = CprjAsync
currentKeys.patientAmbNote['props'] = { encounter_id: id }
}
if (currentKeys?.prescription) {
currentKeys.prescription['component'] = PrescriptionAsync
currentKeys.prescription['props'] = { encounter_id: id }
}
if (currentKeys?.deviceOrder) {
currentKeys.deviceOrder['component'] = DeviceOrderAsync
currentKeys.deviceOrder['props'] = { encounter_id: id }
}
if (currentKeys?.radiologyOrder) {
currentKeys.radiologyOrder['component'] = RadiologyOrderAsync
currentKeys.radiologyOrder['props'] = { encounter_id: id }
}
if (currentKeys?.cpLabOrder) {
currentKeys.cpLabOrder['component'] = CpLabOrderAsync
currentKeys.cpLabOrder['props'] = { encounter_id: id }
}
if (currentKeys?.microLabOrder) {
currentKeys.microLabOrder['component'] = MicroLabOrderAsync
currentKeys.microLabOrder['props'] = { encounter_id: id }
}
if (currentKeys?.paLabOrder) {
// TODO: add component for paLabOrder
currentKeys.paLabOrder['component'] = null
currentKeys.paLabOrder['props'] = { encounter_id: id }
}
if (currentKeys?.procedureRoomOrder) {
currentKeys.procedureRoomOrder['component'] = ProcedureRoomOrderAsync
currentKeys.procedureRoomOrder['props'] = { encounter_id: id }
}
if (currentKeys?.mcuResult) {
// TODO: add component for mcuResult
currentKeys.mcuResult['component'] = null
currentKeys.mcuResult['props'] = { encounter_id: id }
}
if (currentKeys?.consultation) {
currentKeys.consultation['component'] = ConsultationAsync
currentKeys.consultation['props'] = { encounter: data?.encounter }
}
if (currentKeys?.resume) {
currentKeys.resume['component'] = ResumeListAsync
currentKeys.resume['props'] = { encounter_id: id }
}
if (currentKeys?.controlLetter) {
currentKeys.controlLetter['component'] = ControlLetterListAsync
currentKeys.controlLetter['props'] = { encounter: data?.encounter }
}
if (currentKeys?.refBack) {
currentKeys.refBack['component'] = PrbListAsync
currentKeys.refBack['props'] = { encounter: data?.encounter }
}
if (currentKeys?.fkr) {
currentKeys.fkr['component'] = KfrListAsync
currentKeys.fkr['props'] = { encounter: data?.encounter }
}
if (currentKeys?.screening) {
// TODO: add component for screening
currentKeys.screening['component'] = null
currentKeys.screening['props'] = { encounter_id: id }
}
if (currentKeys?.surgeryReport) {
currentKeys.surgeryReport['component'] = SurgeryReportListAsync
currentKeys.surgeryReport['props'] = { encounter: data?.encounter }
}
if (currentKeys?.vaccineData) {
currentKeys.vaccineData['component'] = VaccineDataListAsync
currentKeys.vaccineData['props'] = { encounter: data?.encounter }
}
if (currentKeys?.supportingDocument) {
currentKeys.supportingDocument['component'] = DocUploadListAsync
currentKeys.supportingDocument['props'] = { encounter_id: id }
}
if (currentKeys?.priceList) {
// TODO: add component for priceList
currentKeys.priceList['component'] = null
currentKeys.priceList['props'] = { encounter_id: id }
}
if (currentKeys?.initialNursingStudy) {
currentKeys.initialNursingStudy['component'] = InitialNursingStudyAsync
currentKeys.initialNursingStudy['props'] = { encounter: data?.encounter }
}
return currentKeys
}
export function mergeArrayAt<T>(arraysOne: T[], arraysTwo: T[] | T, deleteCount = 0): T[] {
const prevItems = arraysOne.slice()
if (!prevItems) return prevItems
const nextItems = Array.isArray(arraysTwo) ? arraysTwo : [arraysTwo]
if (nextItems.length === 0) return prevItems
// determine insertion position using the first item's `id` if available
const firstId = (nextItems[0] as any)?.afterId || (prevItems[0] as any)?.id
let pos = prevItems.length
if (typeof firstId === 'string') {
const index = prevItems.findIndex((item: any) => item.id === firstId)
pos = index < 0 ? Math.max(prevItems.length + index, 0) : Math.min(index, prevItems.length)
}
prevItems.splice(pos, deleteCount, ...nextItems)
return prevItems
}
// Function to map API response to Encounter structure
export function mapResponseToEncounter(result: any): any {
if (!result) return null
// Check if patient and patient.person exist (minimal validation)
if (!result.patient || !result.patient.person) {
return null
}
const mapped: any = {
id: result.id || 0,
patient_id: result.patient_id || result.patient?.id || 0,
patient: {
id: result.patient?.id || 0,
number: result.patient?.number || '',
person: {
id: result.patient?.person?.id || 0,
name: result.patient?.person?.name || '',
birthDate: result.patient?.person?.birthDate || null,
gender_code: result.patient?.person?.gender_code || '',
residentIdentityNumber: result.patient?.person?.residentIdentityNumber || null,
frontTitle: result.patient?.person?.frontTitle || '',
endTitle: result.patient?.person?.endTitle || '',
addresses: result.patient?.person?.addresses || [],
},
},
registeredAt: result.registeredAt || result.patient?.registeredAt || null,
class_code: result.class_code || '',
unit_id: result.unit_id || 0,
unit: result.unit || null,
specialist_id: result.specialist_id || null,
subspecialist_id: result.subspecialist_id || null,
visitDate: isValidDate(result.visitDate)
? result.visitDate
: result.registeredAt || result.patient?.registeredAt || null,
adm_employee_id: result.adm_employee_id || 0,
adm_employee: result.adm_employee || null,
responsible_nurse_code: result.responsible_nurse_code || null,
responsible_nurse: result.responsible_nurse || null,
appointment_doctor_code: result.appointment_doctor_code || null,
appointment_doctor: result.appointment_doctor || null,
responsible_doctor_code: result.responsible_doctor_id || null,
responsible_doctor: result.responsible_doctor || null,
refSource_name: result.refSource_name || null,
appointment_id: result.appointment_id || null,
earlyEducation: result.earlyEducation || null,
medicalDischargeEducation: result.medicalDischargeEducation || '',
admDischargeEducation: result.admDischargeEducation || null,
discharge_method_code: result.discharge_method_code || null,
discharge_reason: result.dischargeReason || result.discharge_reason || null,
discharge_date: result.discharge_date || null,
status_code: result.status_code || '',
// Payment related fields
paymentMethod_code:
result.paymentMethod_code && result.paymentMethod_code.trim() !== '' ? result.paymentMethod_code : null,
trx_number: result.trx_number || null,
member_number: result.member_number || null,
ref_number: result.ref_number || null,
}
return mapped
}
export function getMenuItems(
id: string | number,
props: any,
user: any,
data: EncounterListData,
meta: any,
) {
// const normalClassCode = props.classCode === 'ambulatory' ? 'outpatient' : props.classCode
const normalClassCode = props.classCode === 'ambulatory' ? 'ambulatory' : props.classCode
const currentKeys = injectComponents(id, data, meta)
const defaultItems: EncounterItem[] = Object.values(currentKeys)
const listItemsForOutpatientRehab = mergeArrayAt(
getItemsAll('ambulatory', 'all', defaultItems),
getItemsAll('ambulatory', 'rehab', defaultItems),
)
const listItemsForOutpatientChemo = mergeArrayAt(
getItemsAll('ambulatory', 'all', defaultItems),
getItemsAll('ambulatory', 'chemo', defaultItems),
)
const listItems: Record<string, Record<string, Record<string, any>>> = {
'installation|ambulatory': {
'unit|rehab': {
items: listItemsForOutpatientRehab,
roles: medicalRoles,
},
'unit|chemo': {
items: listItemsForOutpatientChemo,
roles: medicalRoles,
},
all: getItemsAll('ambulatory', 'all', defaultItems),
},
'installation|emergency': {
all: getItemsAll('emergency', 'all', defaultItems),
},
'installation|inpatient': {
all: getItemsAll('inpatient', 'all', defaultItems),
},
}
const currentListItems = listItems[`installation|${normalClassCode}`]
if (!currentListItems) return []
const unitCode = user?.unit_code ? `unit|${user.unit_code}` : 'all'
const currentUnitItems: any = currentListItems[`${unitCode}`]
if (!currentUnitItems) return []
let menus = []
if (currentUnitItems.roles && currentUnitItems.roles?.includes(user.activeRole)) {
menus = [...currentUnitItems.items]
} else {
menus = unitCode !== 'all' && currentUnitItems?.items ? [...currentUnitItems.items] : [...currentUnitItems]
}
return menus
}
+24
View File
@@ -0,0 +1,24 @@
// Handlers
import { genCrudHandler } from '~/handlers/_handler'
// Services
import { create, update, remove } from '~/services/item-price.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
+24
View File
@@ -0,0 +1,24 @@
// Handlers
import { genCrudHandler } from '~/handlers/_handler'
// Services
import { create, update, remove } from '~/services/item.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
+3 -4
View File
@@ -3,11 +3,10 @@ import CardContent from '~/components/pub/ui/card/CardContent.vue'
const route = useRoute()
const contentFrame = computed(() => route.meta.contentFrame)
const contentPadding = computed(() => route.meta.contentPadding || 'p-4 2xl:p-5')
const contentUseCard = computed(() => route.meta.contentUseCard === false ? false : true)
console.log(route.meta.contentUseCard,contentUseCard)
const contentUseCard = computed(() => (route.meta.contentUseCard === false ? false : true))
console.log(route.meta.contentUseCard, contentUseCard)
const contentFrameClass = computed(() => {
switch (contentFrame.value) {
case 'cf-container-2xl':
@@ -33,7 +32,7 @@ const contentFrameClass = computed(() => {
<LayoutAppSidebar />
<SidebarInset>
<LayoutHeader />
<div :class="`w-full flex justify-center ${contentPadding} ${contentFrameClass}`">
<div :class="`flex w-full justify-center ${contentPadding} ${contentFrameClass}`">
<div v-if="contentFrame !== 'cf-no-frame'">
<Card v-if="contentUseCard">
<CardContent>
+1
View File
@@ -79,6 +79,7 @@ export const paymentTypes: Record<string, string> = {
jkmm: 'JKMM (Jaminan Kesehatan Mandiri)',
spm: 'SPM (Sistem Pembayaran Mandiri)',
pks: 'PKS (Pembiayaan Kesehatan Sosial)',
umum: 'Umum',
}
export const sepRefTypeCodes: Record<string, string> = {
+5 -12
View File
@@ -19,25 +19,18 @@ export function usePageChecker() {
) {
// Check if user has access to this page, need to use try - catch for proper handling
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
if (!hasAccess) return false
// Define permission-based computed properties
const canCreate = hasCreateAccess(roleAccess)
const canRead = hasReadAccess(roleAccess)
const canUpdate = hasUpdateAccess(roleAccess)
const canDelete = hasDeleteAccess(roleAccess)
switch (type) {
case 'create':
return canCreate
return hasCreateAccess(roleAccess)
case 'read':
return canRead
return hasReadAccess(roleAccess)
case 'update':
return canUpdate
return hasUpdateAccess(roleAccess)
case 'delete':
return canDelete
return hasDeleteAccess(roleAccess)
default:
return false
}
+29
View File
@@ -0,0 +1,29 @@
<script setup lang="ts">
import Error from '~/components/pub/my-ui/error/error.vue'
import Content from '~/components/content/item-price/list.vue'
const route = useRoute()
definePageMeta({
middleware: ['rbac'],
roles: ['emp|reg', 'emp|nur', 'emp|doc', 'emp|miw', 'emp|thr', 'emp|nut', 'emp|pha', 'emp|lab'],
title: 'Daftar Item Harga',
contentFrame: 'cf-container-lg',
})
useHead({
title: () => route.meta.title as string,
})
const canRead = true
</script>
<template>
<template v-if="canRead">
<Content />
</template>
<Error
v-else
:status-code="403"
/>
</template>
+29
View File
@@ -0,0 +1,29 @@
<script setup lang="ts">
import Error from '~/components/pub/my-ui/error/error.vue'
import Content from '~/components/content/item/list.vue'
const route = useRoute()
definePageMeta({
middleware: ['rbac'],
roles: ['emp|reg', 'emp|nur', 'emp|doc', 'emp|miw', 'emp|thr', 'emp|nut', 'emp|pha', 'emp|lab'],
title: 'Daftar Item',
contentFrame: 'cf-container-lg',
})
useHead({
title: () => route.meta.title as string,
})
const canRead = true
</script>
<template>
<template v-if="canRead">
<Content />
</template>
<Error
v-else
:status-code="403"
/>
</template>
+3 -1
View File
@@ -1,3 +1,4 @@
import { isValid } from "date-fns"
import { z } from 'zod'
const ERROR_MESSAGES = {
@@ -16,6 +17,7 @@ const ERROR_MESSAGES = {
}
const ACCEPTED_UPLOAD_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
const isValidationSep = false
const IntegrationEncounterSchema = z
.object({
@@ -116,7 +118,7 @@ const IntegrationEncounterSchema = z
.refine(
(data) => {
// If payment type is jkn and SEP type is selected, then SEP number is required
if (data.paymentMethod_code === 'jkn' && data.sepType && data.sepType.trim() !== '') {
if (isValidationSep && data.paymentMethod_code === 'jkn' && data.sepType && data.sepType.trim() !== '') {
return data.sepNumber && data.sepNumber.trim() !== ''
}
return true
+12
View File
@@ -0,0 +1,12 @@
import { z } from 'zod'
const ItemPriceSchema = z.object({
item_code: z.string({ required_error: 'Item harus diisi' }).min(1, 'Item harus diisi'),
price: z.number({ required_error: 'Harga harus diisi' }).min(0, 'Harga tidak boleh kurang dari 0'),
insuranceCompany_code: z.string({ required_error: 'Perusahaan Asuransi harus diisi' }).min(1, 'Perusahaan Asuransi harus diisi'),
})
type ItemPriceFormData = z.infer<typeof ItemPriceSchema>
export { ItemPriceSchema }
export type { ItemPriceFormData }
+17
View File
@@ -0,0 +1,17 @@
import { z } from 'zod'
const ItemSchema = z.object({
code: z.string({ required_error: 'Kode harus diisi' }).min(1, 'Kode minimum 1 karakter'),
name: z.string({ required_error: 'Nama harus diisi' }).min(1, 'Nama minimum 1 karakter'),
itemGroup_code: z.string({ required_error: 'Item Group harus diisi' }).min(1, 'Item Group harus diisi'),
uom_code: z.string({ required_error: 'UOM harus diisi' }).min(1, 'UOM harus diisi'),
infra_code: z.string({ required_error: 'Infra harus diisi' }).optional(),
stock: z.number({ required_error: 'Stok harus diisi' }).min(0, 'Stok tidak boleh kurang dari 0').optional(),
buyingPrice: z.number({ required_error: 'Harga Beli harus diisi' }).min(0, 'Harga Beli tidak boleh kurang dari 0').optional(),
sellingPrice: z.number({ required_error: 'Harga Jual harus diisi' }).min(0, 'Harga Jual tidak boleh kurang dari 0').optional(),
})
type ItemFormData = z.infer<typeof ItemSchema>
export { ItemSchema }
export type { ItemFormData }
+41
View File
@@ -0,0 +1,41 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/item-group'
const name = 'item-group'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}
export async function getValueLabelList(
params: any = null,
useCodeAsValue = false,
): Promise<{ value: string; label: string }[]> {
let data: { value: string; label: string }[] = []
const result = await getList(params)
if (result.success) {
const resultData = result.body?.data || []
data = resultData.map((item: any) => ({
value: useCodeAsValue ? item.code : item.id ? Number(item.id) : item.code,
label: item.name,
}))
}
return data
}
+25
View File
@@ -0,0 +1,25 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/item-price'
const name = 'item-price'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}
+5 -2
View File
@@ -24,13 +24,16 @@ export function remove(id: number | string) {
return base.remove(path, id, name)
}
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
export async function getValueLabelList(
params: any = null,
useCodeAsValue = false,
): Promise<{ value: string; label: string }[]> {
let data: { value: string; label: string }[] = []
const result = await getList(params)
if (result.success) {
const resultData = result.body?.data || []
data = resultData.map((item: any) => ({
value: item.id ? Number(item.id) : item.code,
value: useCodeAsValue ? item.code : item.id ? Number(item.id) : item.code,
label: item.name,
}))
}