Merge pull request #219 from dikstub-rssa/feat/adj-enc-list-199

Feat - Item + Item Price
This commit is contained in:
Munawwirul Jamal
2025-12-08 11:29:49 +07:00
committed by GitHub
31 changed files with 1765 additions and 319 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,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>
@@ -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
+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>
+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') {
+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,
})
+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,
}))
}