Merge pull request #170 from dikstub-rssa/feat/medicine-form-167
Feat/medicine form 167
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||
import { docTypeCode, supportingDocOpt, type docTypeCodeKey } from '~/lib/constants'
|
||||
import { getValueLabelList as getDoctorLabelList } from '~/services/doctor.service'
|
||||
import { getValueLabelList as getUnitLabelList } from '~/services/unit.service'
|
||||
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import type { Item } from '~/components/pub/my-ui/combobox'
|
||||
|
||||
const props = defineProps<{
|
||||
fieldName?: string
|
||||
label?: string
|
||||
placeholder?: string
|
||||
errors?: FormErrors
|
||||
class?: string
|
||||
selectClass?: string
|
||||
fieldGroupClass?: string
|
||||
labelClass?: string
|
||||
isRequired?: boolean
|
||||
}>()
|
||||
|
||||
const {
|
||||
fieldName = 'job',
|
||||
label = 'Pekerjaan',
|
||||
placeholder = 'Pilih pekerjaan',
|
||||
errors,
|
||||
class: containerClass,
|
||||
fieldGroupClass,
|
||||
labelClass,
|
||||
} = props
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||
<DE.Label
|
||||
:label-for="fieldName"
|
||||
:class="cn('select-field-label', labelClass)"
|
||||
:is-required="isRequired"
|
||||
>
|
||||
{{ label }}
|
||||
</DE.Label>
|
||||
<DE.Field
|
||||
:id="fieldName"
|
||||
:errors="errors"
|
||||
:class="cn('select-field-wrapper')"
|
||||
>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
:name="fieldName"
|
||||
>
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
class="focus:ring-0 focus:ring-offset-0"
|
||||
:id="fieldName"
|
||||
v-bind="componentField"
|
||||
:items="supportingDocOpt"
|
||||
:placeholder="placeholder"
|
||||
search-placeholder="Cari..."
|
||||
empty-message="Data tidak ditemukan"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
</template>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { Form } from '~/components/pub/ui/form'
|
||||
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import FileField from '~/components/pub/my-ui/form/file-field.vue'
|
||||
import SelectDocType from './_common/select-doc-type.vue'
|
||||
import Separator from '~/components/pub/ui/separator/Separator.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
schema: any
|
||||
initialValues?: any
|
||||
errors?: FormErrors
|
||||
}>()
|
||||
|
||||
const formSchema = toTypedSchema(props.schema)
|
||||
const formRef = ref()
|
||||
|
||||
defineExpose({
|
||||
validate: () => formRef.value?.validate(),
|
||||
resetForm: () => formRef.value?.resetForm(),
|
||||
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
|
||||
values: computed(() => formRef.value?.values),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
ref="formRef"
|
||||
v-slot="{ values }"
|
||||
as=""
|
||||
keep-values
|
||||
:validation-schema="formSchema"
|
||||
:validate-on-mount="false"
|
||||
validation-mode="onSubmit"
|
||||
:initial-values="initialValues ? initialValues : {}"
|
||||
>
|
||||
<DE.Block :col-count="2" :cell-flex="false">
|
||||
<InputBase
|
||||
field-name="officer"
|
||||
label="Petugas Upload"
|
||||
placeholder="Masukkan Petugas Upload"
|
||||
:is-disabled="true"
|
||||
/>
|
||||
<DE.Cell :col-span="2">
|
||||
<Separator class="w-full my-4"/>
|
||||
<DE.Block :col-count="3" :cell-flex="false">
|
||||
<InputBase
|
||||
field-name="name"
|
||||
label="Nama Dokumen"
|
||||
placeholder="Maukkan Nama Dokumen"
|
||||
/>
|
||||
<SelectDocType
|
||||
field-name="type_code"
|
||||
label="Tipe Dokumen"
|
||||
placeholder="Pilih Jenis Dokumen"
|
||||
:errors="errors"
|
||||
is-required
|
||||
/>
|
||||
<FileField
|
||||
field-name="content"
|
||||
label="Upload Dokumen"
|
||||
placeholder="Unggah dokumen"
|
||||
:errors="errors"
|
||||
:accept="['pdf', 'jpg', 'png']"
|
||||
:max-size-mb="1"
|
||||
/>
|
||||
</DE.Block>
|
||||
</DE.Cell>
|
||||
</DE.Block>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { docTypeCode, docTypeLabel, type docTypeCodeKey } from '~/lib/constants'
|
||||
|
||||
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dd.vue'))
|
||||
|
||||
export const config: Config = {
|
||||
cols: [{}, {}, {}, {width: 50},],
|
||||
|
||||
headers: [
|
||||
[
|
||||
{ label: 'Nama Dokumen' },
|
||||
{ label: 'Tipe Dokumen' },
|
||||
{ label: 'Petugas Upload' },
|
||||
{ label: 'Action' },
|
||||
],
|
||||
],
|
||||
|
||||
keys: ['fileName', 'type_code', 'employee.name', 'action'],
|
||||
|
||||
delKeyNames: [
|
||||
|
||||
],
|
||||
|
||||
parses: {
|
||||
type_code: (v: unknown) => {
|
||||
return docTypeLabel[v?.type_code as docTypeCodeKey]
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
action(rec, idx) {
|
||||
return {
|
||||
idx,
|
||||
rec: rec as object,
|
||||
component: action,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
htmls: {
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||
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">
|
||||
<PubMyUiDataTable
|
||||
v-bind="config"
|
||||
:rows="data"
|
||||
:skeleton-size="paginationMeta?.pageSize"
|
||||
/>
|
||||
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import Block from '~/components/pub/my-ui/doc-entry/block.vue'
|
||||
import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
|
||||
import Field from '~/components/pub/my-ui/doc-entry/field.vue'
|
||||
import Label from '~/components/pub/my-ui/doc-entry/label.vue'
|
||||
import Button from '~/components/pub/ui/button/Button.vue'
|
||||
|
||||
// Types
|
||||
import type { BaseFormData } from '~/schemas/base.schema'
|
||||
|
||||
// Helpers
|
||||
import type z from 'zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
|
||||
interface Props {
|
||||
schema?: z.ZodSchema<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: BaseFormData, resetForm: () => void]
|
||||
cancel: [resetForm: () => void]
|
||||
}>()
|
||||
|
||||
const { defineField, errors, meta } = useForm({
|
||||
validationSchema: props.schema ? toTypedSchema(props.schema) : undefined,
|
||||
initialValues: {
|
||||
code: '',
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const [code, codeAttrs] = defineField('code')
|
||||
const [name, nameAttrs] = defineField('name')
|
||||
|
||||
if (props.values) {
|
||||
if (props.values.code !== undefined) code.value = props.values.code
|
||||
if (props.values.name !== undefined) name.value = props.values.name
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
code.value = ''
|
||||
name.value = ''
|
||||
}
|
||||
|
||||
function onSubmitForm() {
|
||||
const formData = {
|
||||
name: name.value || '',
|
||||
code: code.value || '',
|
||||
}
|
||||
emit('submit', formData, resetForm)
|
||||
}
|
||||
|
||||
function onCancelForm() {
|
||||
emit('cancel', resetForm)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
id="form-medicine-group"
|
||||
@submit.prevent
|
||||
>
|
||||
<Block
|
||||
labelSize="thin"
|
||||
class="!mb-2.5 !pt-0 xl:!mb-3"
|
||||
:colCount="1"
|
||||
>
|
||||
<Cell>
|
||||
<Label height="">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>
|
||||
</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>
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
|
||||
|
||||
export const config: Config = {
|
||||
cols: [{}, {}, { width: 50 }],
|
||||
|
||||
headers: [
|
||||
[
|
||||
{ label: 'Kode' },
|
||||
{ label: 'Nama' },
|
||||
{ label: 'Aksi' },
|
||||
],
|
||||
],
|
||||
|
||||
keys: ['code', 'name', 'action'],
|
||||
|
||||
delKeyNames: [
|
||||
{ key: 'code', label: 'Kode' },
|
||||
{ key: 'name', label: 'Nama' },
|
||||
],
|
||||
|
||||
parses: {},
|
||||
|
||||
components: {
|
||||
action(rec, idx) {
|
||||
const res: RecComponent = {
|
||||
idx,
|
||||
rec: rec as object,
|
||||
component: action,
|
||||
}
|
||||
return res
|
||||
},
|
||||
},
|
||||
|
||||
htmls: {},
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<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'
|
||||
|
||||
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">
|
||||
<PubMyUiDataTable
|
||||
v-bind="config"
|
||||
:rows="data"
|
||||
/>
|
||||
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,6 +18,7 @@ interface Props {
|
||||
isReadonly?: boolean
|
||||
medicineGroups?: { value: string; label: string }[]
|
||||
medicineMethods?: { value: string; label: string }[]
|
||||
medicineForms?: { value: string; label: string }[]
|
||||
uoms?: { value: string; label: string }[]
|
||||
}
|
||||
|
||||
@@ -36,6 +37,7 @@ const { defineField, errors, meta } = useForm({
|
||||
name: '',
|
||||
medicineGroup_code: '',
|
||||
medicineMethod_code: '',
|
||||
medicineForm_code: '',
|
||||
uom_code: '',
|
||||
stock: 0,
|
||||
},
|
||||
@@ -45,6 +47,7 @@ const [code, codeAttrs] = defineField('code')
|
||||
const [name, nameAttrs] = defineField('name')
|
||||
const [medicineGroup_code, medicineGroupAttrs] = defineField('medicineGroup_code')
|
||||
const [medicineMethod_code, medicineMethodAttrs] = defineField('medicineMethod_code')
|
||||
const [medicineForm_code, medicineFormAttrs] = defineField('medicineForm_code')
|
||||
const [uom_code, uomAttrs] = defineField('uom_code')
|
||||
const [stock, stockAttrs] = defineField('stock')
|
||||
|
||||
@@ -53,6 +56,7 @@ if (props.values) {
|
||||
if (props.values.name !== undefined) name.value = props.values.name
|
||||
if (props.values.medicineGroup_code !== undefined) medicineGroup_code.value = props.values.medicineGroup_code
|
||||
if (props.values.medicineMethod_code !== undefined) medicineMethod_code.value = props.values.medicineMethod_code
|
||||
if (props.values.medicineForm_code !== undefined) medicineForm_code.value = props.values.medicineForm_code
|
||||
if (props.values.uom_code !== undefined) uom_code.value = props.values.uom_code
|
||||
if (props.values.stock !== undefined) stock.value = props.values.stock
|
||||
}
|
||||
@@ -62,6 +66,7 @@ const resetForm = () => {
|
||||
name.value = ''
|
||||
medicineGroup_code.value = ''
|
||||
medicineMethod_code.value = ''
|
||||
medicineForm_code.value = '',
|
||||
uom_code.value = ''
|
||||
stock.value = 0
|
||||
}
|
||||
@@ -72,6 +77,7 @@ function onSubmitForm() {
|
||||
name: name.value || '',
|
||||
medicineGroup_code: medicineGroup_code.value || '',
|
||||
medicineMethod_code: medicineMethod_code.value || '',
|
||||
medicineForm_code: medicineForm_code.value || '',
|
||||
uom_code: uom_code.value || '',
|
||||
stock: stock.value || 0,
|
||||
}
|
||||
@@ -138,6 +144,20 @@ function onCancelForm() {
|
||||
/>
|
||||
</Field>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Label height="compact">Sediaan Obat</Label>
|
||||
<Field :errMessage="errors.medicineForm_code">
|
||||
<Select
|
||||
id="medicineForm_code"
|
||||
v-model="medicineForm_code"
|
||||
icon-name="i-lucide-chevron-down"
|
||||
placeholder="Pilih metode pemberian"
|
||||
v-bind="medicineFormAttrs"
|
||||
:items="props.medicineForms || []"
|
||||
:disabled="isLoading || isReadonly"
|
||||
/>
|
||||
</Field>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Label>Satuan</Label>
|
||||
<Field :errMessage="errors.uom_code">
|
||||
|
||||
@@ -15,12 +15,13 @@ export const config: Config = {
|
||||
{ label: 'Golongan' },
|
||||
{ label: 'Metode Pemberian' },
|
||||
{ label: 'Satuan' },
|
||||
{ label: 'Sediaan' },
|
||||
{ label: 'Stok' },
|
||||
{ label: 'Aksi' },
|
||||
],
|
||||
],
|
||||
|
||||
keys: ['code', 'name', 'group', 'method', 'unit', 'stock', 'action'],
|
||||
keys: ['code', 'name', 'group', 'method', 'unit', 'form', 'stock', 'action'],
|
||||
|
||||
delKeyNames: [
|
||||
{ key: 'code', label: 'Kode' },
|
||||
@@ -37,6 +38,9 @@ export const config: Config = {
|
||||
unit: (rec: unknown): unknown => {
|
||||
return (rec as SmallDetailDto).uom?.name || '-'
|
||||
},
|
||||
form: (rec: unknown): unknown => {
|
||||
return (rec as SmallDetailDto).medicineForm?.name || '-'
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import { handleActionSave,} from '~/handlers/supporting-document.handler'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import { DocumentUploadSchema } from '~/schemas/document-upload.schema'
|
||||
import { uploadAttachment } from '~/services/supporting-document.service'
|
||||
import { printFormData, toFormData } from '~/lib/utils'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = defineProps<{
|
||||
callbackUrl?: string
|
||||
}>()
|
||||
|
||||
// form related state
|
||||
const route = useRoute()
|
||||
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||
const inputForm = ref<ExposedForm<any> | null>(null)
|
||||
const { user } = useUserStore()
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const router = useRouter()
|
||||
const isConfirmationOpen = ref(false)
|
||||
const initialValues = {
|
||||
officer: user.user_name,
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
|
||||
async function handleConfirmAdd() {
|
||||
const inputData = await composeFormData()
|
||||
const inputFormData: FormData = toFormData(inputData)
|
||||
|
||||
const response = await handleActionSave(inputFormData, () => { }, () => { }, toast, )
|
||||
const data = (response?.body?.data ?? null)
|
||||
if (!data) return
|
||||
|
||||
// // If has callback provided redirect to callback with patientData
|
||||
if (props.callbackUrl) {
|
||||
navigateTo(props.callbackUrl + '?control-letter-id=' + inputData.id)
|
||||
}
|
||||
goBack()
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<any> {
|
||||
inputForm.value?.setValues({
|
||||
...inputForm.value?.values,
|
||||
ref_id: encounterId,
|
||||
upload_employee_id: user.employee_id
|
||||
})
|
||||
|
||||
const [inputFormState,] = await Promise.all([
|
||||
inputForm.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [inputFormState]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
// exit, if form errors happend during validation
|
||||
if (!allValid) {
|
||||
toast({ title: 'Form validation failed', variant: 'destructive',})
|
||||
return Promise.reject('Form validation failed')
|
||||
}
|
||||
const formData = inputFormState?.values
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
isConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
if (eventType === 'back') {
|
||||
if (props.callbackUrl) {
|
||||
await navigateTo(props.callbackUrl)
|
||||
return
|
||||
}
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelAdd() {
|
||||
isConfirmationOpen.value = false
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">
|
||||
<h1>Upload Dokumen</h1>
|
||||
</div>
|
||||
<AppDocumentUploadEntryForm
|
||||
ref="inputForm"
|
||||
:schema="DocumentUploadSchema"
|
||||
:initial-values="initialValues"
|
||||
/>
|
||||
|
||||
<div class="mb-2 flex justify-end py-2">
|
||||
<Action :enable-draft="false" @click="handleActionClick" />
|
||||
</div>
|
||||
|
||||
<Confirmation v-model:open="isConfirmationOpen"
|
||||
title="Simpan Data"
|
||||
message="Apakah Anda yakin ingin menyimpan data ini?"
|
||||
confirm-text="Simpan"
|
||||
@confirm="handleConfirmAdd"
|
||||
@cancel="handleCancelAdd" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* component style */
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import { handleActionSave,} from '~/handlers/supporting-document.handler'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import { DocumentUploadSchema } from '~/schemas/document-upload.schema'
|
||||
import { getDetail } from '~/services/supporting-document.service'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = defineProps<{
|
||||
callbackUrl?: string
|
||||
}>()
|
||||
|
||||
// form related state
|
||||
const route = useRoute()
|
||||
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||
const docId = typeof route.params.document_id == 'string' ? parseInt(route.params.document_id) : 0
|
||||
const inputForm = ref<ExposedForm<any> | null>(null)
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const router = useRouter()
|
||||
const isConfirmationOpen = ref(false)
|
||||
const { user } = useUserStore()
|
||||
const initialValues = {
|
||||
officer: user.user_name,
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(async () => {
|
||||
const result = await getDetail(docId)
|
||||
if (result.success) {
|
||||
inputForm.value?.setValues(result.body.data)
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
async function handleConfirmAdd() {
|
||||
const inputData = await composeFormData()
|
||||
let createdDataId = 0
|
||||
|
||||
// const response = await handleActionSave(
|
||||
// inputData,
|
||||
// () => { },
|
||||
// () => { },
|
||||
// toast,
|
||||
// )
|
||||
|
||||
// const data = (response?.body?.data ?? null)
|
||||
// if (!data) return
|
||||
// createdDataId = data.id
|
||||
|
||||
// // // If has callback provided redirect to callback with patientData
|
||||
// if (props.callbackUrl) {
|
||||
// navigateTo(props.callbackUrl + '?control-letter-id=' + inputData.id)
|
||||
// }
|
||||
// goBack()
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<any> {
|
||||
const [inputFormState,] = await Promise.all([
|
||||
inputForm.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [inputFormState]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
// exit, if form errors happend during validation
|
||||
if (!allValid) return Promise.reject('Form validation failed')
|
||||
|
||||
const formData = inputFormState?.values
|
||||
formData.encounter_id = encounterId
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
isConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
if (eventType === 'back') {
|
||||
if (props.callbackUrl) {
|
||||
await navigateTo(props.callbackUrl)
|
||||
return
|
||||
}
|
||||
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelAdd() {
|
||||
isConfirmationOpen.value = false
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">
|
||||
<h1>Upload Dokumen</h1>
|
||||
</div>
|
||||
<AppDocumentUploadEntryForm
|
||||
ref="inputForm"
|
||||
:schema="DocumentUploadSchema"
|
||||
:initial-values="initialValues"
|
||||
/>
|
||||
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action :enable-draft="false" @click="handleActionClick" />
|
||||
</div>
|
||||
|
||||
<Confirmation v-model:open="isConfirmationOpen"
|
||||
title="Simpan Data"
|
||||
message="Apakah Anda yakin ingin menyimpan data ini?"
|
||||
confirm-text="Simpan"
|
||||
@confirm="handleConfirmAdd"
|
||||
@cancel="handleCancelAdd" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* component style */
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
// #region Imports
|
||||
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||
import type { HeaderPrep, RefSearchNav, } from '~/components/pub/my-ui/data/types'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import { getList, remove } from '~/services/supporting-document.service'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
// #endregion
|
||||
|
||||
// #region State
|
||||
const props = defineProps<{
|
||||
encounter?: Encounter
|
||||
refresh: () => void
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||
|
||||
const { data, paginationMeta, handlePageChange, handleSearch, searchInput, fetchData } = usePaginatedList({
|
||||
fetchFn: (params) => getList({
|
||||
'encounter-id': encounterId,
|
||||
// includes: "employee",
|
||||
...params,
|
||||
}),
|
||||
entityName: 'encounter-document',
|
||||
})
|
||||
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const timestamp = ref<number>(0)
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: "Upload Dokumen",
|
||||
icon: 'i-lucide-newspaper',
|
||||
addNav: {
|
||||
label: "Upload Dokumen",
|
||||
onClick: () => navigateTo({
|
||||
name: 'rehab-encounter-id-document-upload-add',
|
||||
params: { id: encounterId },
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
// open filter modal
|
||||
},
|
||||
onInput: (val: string) => {
|
||||
searchInput.value = val
|
||||
},
|
||||
onClear: () => {
|
||||
searchInput.value = ''
|
||||
},
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
|
||||
async function handleConfirmDelete(record: any, action: string) {
|
||||
if (action === 'delete' && record?.id) {
|
||||
try {
|
||||
const result = await remove(record.id)
|
||||
if (result.success) {
|
||||
toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
|
||||
fetchData()
|
||||
} else {
|
||||
toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleCancelConfirmation() {
|
||||
// Reset record state when cancelled
|
||||
recId.value = 0
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Provide
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('timestamp', timestamp)
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
watch([recId, recAction, timestamp], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
navigateTo(recItem.value.filePath, { external: true, open: { target: '_blank' } })
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
navigateTo({
|
||||
name: 'rehab-encounter-id-document-upload-document_id-edit',
|
||||
params: { id: encounterId, "document_id": recId.value },
|
||||
})
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...headerPrep }"
|
||||
v-model:search="searchInput"
|
||||
:ref-search-nav="refSearchNav"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<AppDocumentUploadList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange"/>
|
||||
|
||||
<RecordConfirmation v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
|
||||
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation">
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.name">
|
||||
<strong>Nama:</strong>
|
||||
{{ record?.name }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -18,6 +18,8 @@ import Prescription from '~/components/content/prescription/main.vue'
|
||||
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 DocUploadList from '~/components/content/document-upload/list.vue'
|
||||
import { genEncounter } from '~/models/encounter'
|
||||
import GeneralConsentList from '~/components/content/general-consent/entry.vue'
|
||||
import ResumeList from '~/components/content/resume/list.vue'
|
||||
import ControlLetterList from '~/components/content/control-letter/list.vue'
|
||||
@@ -34,12 +36,18 @@ const activeTab = computed({
|
||||
})
|
||||
|
||||
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||
const dataRes = await getDetail(id, {
|
||||
includes:
|
||||
'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person',
|
||||
const data = ref(genEncounter())
|
||||
|
||||
async function fetchDetail() {
|
||||
const res = await getDetail(id, {
|
||||
includes: 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person,EncounterDocuments',
|
||||
})
|
||||
if(res.body?.data) data.value = res.body?.data
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDetail()
|
||||
})
|
||||
const dataResBody = dataRes.body ?? null
|
||||
const data = dataResBody?.data ?? null
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{ value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } },
|
||||
@@ -65,10 +73,10 @@ const tabs: TabItem[] = [
|
||||
{ value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' },
|
||||
{ value: 'consent', label: 'General Consent', component: GeneralConsentList, props: { encounter: data } },
|
||||
{ value: 'patient-note', label: 'CPRJ' },
|
||||
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.id } },
|
||||
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.value.id } },
|
||||
{ value: 'device', label: 'Order Alkes' },
|
||||
{ value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.id } },
|
||||
{ value: 'mcu-lab-cp', label: 'Order Lab PK', component: CpLabOrder, props: { encounter_id: data.id } },
|
||||
{ value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.value.id } },
|
||||
{ value: 'mcu-lab-cp', label: 'Order Lab PK', component: CpLabOrder, props: { encounter_id: data.value.id } },
|
||||
{ value: 'mcu-lab-micro', label: 'Order Lab Mikro' },
|
||||
{ value: 'mcu-lab-pa', label: 'Order Lab PA' },
|
||||
{ value: 'medical-action', label: 'Order Ruang Tindakan' },
|
||||
@@ -79,7 +87,7 @@ const tabs: TabItem[] = [
|
||||
{ value: 'resume', label: 'Resume' },
|
||||
{ value: 'control', label: 'Surat Kontrol', component: ControlLetterList, props: { encounter: data } },
|
||||
{ value: 'screening', label: 'Skrinning MPP' },
|
||||
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung' },
|
||||
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung', component: DocUploadList, props: { encounter: data }, },
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ watch([recId, recAction], () => {
|
||||
getCurrentMaterialDetail(recId.value)
|
||||
title.value = 'Edit Perlengkapan'
|
||||
isReadonly.value = false
|
||||
isFormEntryDialogOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
@@ -158,7 +159,7 @@ onMounted(async () => {
|
||||
@submit="
|
||||
(values: MaterialFormData, resetForm: any) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getEquipmentList, resetForm, toast)
|
||||
handleActionEdit(recItem.code, values, getEquipmentList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getEquipmentList, resetForm, toast)
|
||||
@@ -173,7 +174,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getEquipmentList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getEquipmentList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import AppMedicineFormList from '~/components/app/medicine-form/list.vue'
|
||||
import AppMedicineFormEntryForm from '~/components/app/medicine-form/entry-form.vue'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// Types
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import { BaseSchema, type BaseFormData } from '~/schemas/base.schema'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/medicine-form.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/medicine-form.service'
|
||||
|
||||
const title = ref('')
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getMedicineFormList,
|
||||
} = 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: 'medicine-form',
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Sediaan Obat',
|
||||
icon: 'i-lucide-medicine-bottle',
|
||||
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
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
const getCurrentMedicineFormDetail = 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:
|
||||
getCurrentMedicineFormDetail(recId.value)
|
||||
title.value = 'Detail Sediaan Obat'
|
||||
isReadonly.value = true
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentMedicineFormDetail(recId.value)
|
||||
title.value = 'Edit Sediaan Obat'
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await getMedicineFormList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
v-model="searchInput"
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<AppMedicineFormList
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
v-model:open="isFormEntryDialogOpen"
|
||||
:title="!!recItem ? title : 'Tambah Sediaan Obat'"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
@update:open="
|
||||
(value: any) => {
|
||||
onResetState()
|
||||
isFormEntryDialogOpen = value
|
||||
}
|
||||
"
|
||||
>
|
||||
<AppMedicineFormEntryForm
|
||||
:schema="BaseSchema"
|
||||
:values="recItem"
|
||||
:is-loading="isProcessing"
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: BaseFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recItem.code, values, getMedicineFormList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getMedicineFormList, resetForm, toast)
|
||||
}
|
||||
"
|
||||
@cancel="handleCancelForm"
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
<!-- Record Confirmation Modal -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recItem.code, getMedicineFormList, 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>
|
||||
@@ -108,6 +108,7 @@ watch([recId, recAction], () => {
|
||||
getCurrentMedicineGroupDetail(recId.value)
|
||||
title.value = 'Edit Kelompok Obat'
|
||||
isReadonly.value = false
|
||||
isFormEntryDialogOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
@@ -154,7 +155,7 @@ onMounted(async () => {
|
||||
@submit="
|
||||
(values: BaseFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getMedicineGroupList, resetForm, toast)
|
||||
handleActionEdit(recItem.code, values, getMedicineGroupList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getMedicineGroupList, resetForm, toast)
|
||||
@@ -169,7 +170,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMedicineGroupList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getMedicineGroupList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -108,6 +108,7 @@ watch([recId, recAction], () => {
|
||||
getCurrentMedicineMethodDetail(recId.value)
|
||||
title.value = 'Edit Metode Obat'
|
||||
isReadonly.value = false
|
||||
isFormEntryDialogOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
@@ -154,7 +155,7 @@ onMounted(async () => {
|
||||
@submit="
|
||||
(values: BaseFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getMedicineMethodList, resetForm, toast)
|
||||
handleActionEdit(recItem.code, values, getMedicineMethodList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getMedicineMethodList, resetForm, toast)
|
||||
@@ -169,7 +170,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMedicineMethodList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getMedicineMethodList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -37,10 +37,12 @@ import {
|
||||
import { getList, getDetail } from '~/services/medicine.service'
|
||||
import { getValueLabelList as getMedicineGroupList } from '~/services/medicine-group.service'
|
||||
import { getValueLabelList as getMedicineMethodList } from '~/services/medicine-method.service'
|
||||
import { getValueLabelList as getMedicineFormList } from '~/services/medicine-form.service'
|
||||
import { getValueLabelList as getUomList } from '~/services/uom.service'
|
||||
|
||||
const medicineGroups = ref<{ value: string; label: string }[]>([])
|
||||
const medicineMethods = ref<{ value: string; label: string }[]>([])
|
||||
const medicineForms = ref<{ value: string; label: string }[]>([])
|
||||
const uoms = ref<{ value: string; label: string }[]>([])
|
||||
const title = ref('')
|
||||
|
||||
@@ -59,7 +61,7 @@ const {
|
||||
sort: 'createdAt:asc',
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
includes: 'medicineGroup,medicineMethod,uom',
|
||||
includes: 'medicineGroup,medicineMethod,medicineForm,uom',
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
@@ -116,6 +118,7 @@ watch([recId, recAction], () => {
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentMedicineDetail(recId.value)
|
||||
title.value = 'Edit Obat'
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
@@ -127,6 +130,7 @@ watch([recId, recAction], () => {
|
||||
onMounted(async () => {
|
||||
medicineGroups.value = await getMedicineGroupList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||
medicineMethods.value = await getMedicineMethodList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||
medicineForms.value = await getMedicineFormList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||
uoms.value = await getUomList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||
await getMedicineList()
|
||||
})
|
||||
@@ -163,13 +167,14 @@ onMounted(async () => {
|
||||
:values="recItem"
|
||||
:medicineGroups="medicineGroups"
|
||||
:medicineMethods="medicineMethods"
|
||||
:medicineForms="medicineForms"
|
||||
:uoms="uoms"
|
||||
:is-loading="isProcessing"
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: MedicineFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getMedicineList, resetForm, toast)
|
||||
handleActionEdit(recItem.code, values, getMedicineList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getMedicineList, resetForm, toast)
|
||||
@@ -184,7 +189,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMedicineList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getMedicineList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -163,7 +163,7 @@ onMounted(async () => {
|
||||
@submit="
|
||||
(values: DeviceFormData, resetForm: any) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getToolsList, resetForm, toast)
|
||||
handleActionEdit(recItem.code, values, getToolsList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getToolsList, resetForm, toast)
|
||||
@@ -178,7 +178,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getToolsList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getToolsList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import type { LinkItem, ListItemDto } from './types'
|
||||
import { ActionEvents } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
rec: ListItemDto
|
||||
}>()
|
||||
|
||||
const recId = inject<Ref<number>>('rec_id')!
|
||||
const recAction = inject<Ref<string>>('rec_action')!
|
||||
const recItem = inject<Ref<any>>('rec_item')!
|
||||
const timestamp = inject<Ref<number>>('timestamp')!
|
||||
const activeKey = ref<string | null>(null)
|
||||
const linkItems: LinkItem[] = [
|
||||
{
|
||||
label: 'Detail',
|
||||
onClick: () => {
|
||||
detail()
|
||||
},
|
||||
icon: 'i-lucide-eye',
|
||||
},
|
||||
{
|
||||
label: 'Hapus',
|
||||
onClick: () => {
|
||||
del()
|
||||
},
|
||||
icon: 'i-lucide-trash',
|
||||
},
|
||||
]
|
||||
|
||||
function detail() {
|
||||
recId.value = props.rec.id || 0
|
||||
recAction.value = ActionEvents.showDetail
|
||||
recItem.value = props.rec
|
||||
timestamp.value = Date.now()
|
||||
}
|
||||
|
||||
function del() {
|
||||
recId.value = props.rec.id || 0
|
||||
recAction.value = ActionEvents.showConfirmDelete
|
||||
recItem.value = props.rec
|
||||
timestamp.value = Date.now()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-chevrons-up-down"
|
||||
class="ml-auto size-4"
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
v-for="item in linkItems"
|
||||
:key="item.label"
|
||||
class="hover:bg-gray-100 dark:hover:bg-slate-700"
|
||||
@click="item.onClick"
|
||||
@mouseenter="activeKey = item.label"
|
||||
@mouseleave="activeKey = null"
|
||||
>
|
||||
<Icon :name="item.icon ?? ''" />
|
||||
<span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</template>
|
||||
@@ -62,7 +62,7 @@ async function onFileChange(event: Event, handleChange: (value: any) => void) {
|
||||
@change="onFileChange($event, handleChange)"
|
||||
type="file"
|
||||
:disabled="isDisabled"
|
||||
v-bind="componentField"
|
||||
v-bind="{ onBlur: componentField.onBlur }"
|
||||
:placeholder="placeholder"
|
||||
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
||||
/>
|
||||
|
||||
@@ -43,4 +43,4 @@ function onClick(type: ClickType) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { createCrudHandler } from '~/handlers/_handler'
|
||||
import { create, update, remove } from '~/services/medicine-form.service'
|
||||
|
||||
export const {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} = createCrudHandler({
|
||||
post: create,
|
||||
patch: update,
|
||||
remove: remove,
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
// Handlers
|
||||
import { genCrudHandler } from '~/handlers/_handler'
|
||||
|
||||
// Services
|
||||
import { create, update, remove } from '~/services/supporting-document.service'
|
||||
|
||||
export const {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} = genCrudHandler({
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
})
|
||||
@@ -383,3 +383,45 @@ export const medicalActionTypeCode: Record<string, string> = {
|
||||
} as const
|
||||
|
||||
export type medicalActionTypeCodeKey = keyof typeof medicalActionTypeCode
|
||||
|
||||
export const encounterDocTypeCode: Record<string, string> = {
|
||||
"person-resident-number": 'person-resident-number',
|
||||
"person-driving-license": 'person-driving-license',
|
||||
"person-passport": 'person-passport',
|
||||
"person-family-card": 'person-family-card',
|
||||
"mcu-item-result": 'mcu-item-result',
|
||||
"vclaim-sep": 'vclaim-sep',
|
||||
"vclaim-sipp": 'vclaim-sipp',
|
||||
} as const
|
||||
export type encounterDocTypeCodeKey = keyof typeof encounterDocTypeCode
|
||||
export const encounterDocOpt: { label: string; value: encounterDocTypeCodeKey }[] = [
|
||||
{ label: 'KTP', value: 'person-resident-number' },
|
||||
{ label: 'SIM', value: 'person-driving-license' },
|
||||
{ label: 'Passport', value: 'person-passport' },
|
||||
{ label: 'Kartu Keluarga', value: 'person-family-card' },
|
||||
{ label: 'Hasil MCU', value: 'mcu-item-result' },
|
||||
{ label: 'Klaim SEP', value: 'vclaim-sep' },
|
||||
{ label: 'Klaim SIPP', value: 'vclaim-sipp' },
|
||||
]
|
||||
|
||||
|
||||
export const docTypeCode = {
|
||||
"encounter-patient": 'encounter-patient',
|
||||
"encounter-support": 'encounter-support',
|
||||
"encounter-other": 'encounter-other',
|
||||
"vclaim-sep": 'vclaim-sep',
|
||||
"vclaim-sipp": 'vclaim-sipp',
|
||||
} as const
|
||||
export const docTypeLabel = {
|
||||
"encounter-patient": 'Data Pasien',
|
||||
"encounter-support": 'Data Penunjang',
|
||||
"encounter-other": 'Lain - Lain',
|
||||
"vclaim-sep": 'SEP',
|
||||
"vclaim-sipp": 'SIPP',
|
||||
} as const
|
||||
export type docTypeCodeKey = keyof typeof docTypeCode
|
||||
export const supportingDocOpt = [
|
||||
{ label: 'Data Pasien', value: 'encounter-patient' },
|
||||
{ label: 'Data Penunjang', value: 'encounter-support' },
|
||||
{ label: 'Lain - Lain', value: 'encounter-other' },
|
||||
]
|
||||
|
||||
+49
-1
@@ -106,10 +106,58 @@ export function calculateAge(birthDate: Date | string | null | undefined): strin
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a plain JavaScript object (including File objects) into a FormData instance.
|
||||
* @param {object} data - The object to convert (e.g., form values).
|
||||
* @returns {FormData} The new FormData object suitable for API submission.
|
||||
*/
|
||||
export function toFormData(data: Record<string, any>): FormData {
|
||||
const formData = new FormData();
|
||||
|
||||
for (const key in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
const value = data[key];
|
||||
|
||||
// Handle File objects, Blobs, or standard JSON values
|
||||
if (value !== null && value !== undefined) {
|
||||
// Check if the value is a File/Blob instance
|
||||
if (value instanceof File || value instanceof Blob) {
|
||||
// Append the file directly
|
||||
formData.append(key, value);
|
||||
} else if (typeof value === 'object') {
|
||||
// Handle nested objects/arrays by stringifying them (optional, depends on API)
|
||||
// Note: Most APIs expect nested data to be handled separately or passed as JSON string
|
||||
// For simplicity, we stringify non-File objects.
|
||||
formData.append(key, JSON.stringify(value));
|
||||
} else {
|
||||
// Append standard string, number, or boolean values
|
||||
formData.append(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
export function printFormData(formData: FormData) {
|
||||
console.log("--- FormData Contents ---");
|
||||
// Use the entries() iterator to loop through key/value pairs
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value instanceof File) {
|
||||
console.log(`Key: ${key}, Value: [File: ${value.name}, Type: ${value.type}, Size: ${value.size} bytes]`);
|
||||
} else {
|
||||
console.log(`Key: ${key}, Value: "${value}"`);
|
||||
}
|
||||
}
|
||||
console.log("-------------------------");
|
||||
}
|
||||
|
||||
export function unauthorizedToast() {
|
||||
toast({
|
||||
title: 'Unauthorized',
|
||||
description: 'You are not authorized to perform this action.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { type Base, genBase } from "./_base"
|
||||
import { docTypeLabel, } from '~/lib/constants'
|
||||
import { genEmployee, type Employee } from "./employee"
|
||||
import { genEncounter, type Encounter } from "./encounter"
|
||||
|
||||
export interface EncounterDocument extends Base {
|
||||
encounter_id: number
|
||||
encounter?: Encounter
|
||||
upload_employee_id: number
|
||||
employee?: Employee
|
||||
type_code: string
|
||||
name: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
}
|
||||
|
||||
export function genEncounterDocument(): EncounterDocument {
|
||||
return {
|
||||
...genBase(),
|
||||
encounter_id: 2,
|
||||
encounter: genEncounter(),
|
||||
upload_employee_id: 0,
|
||||
employee: genEmployee(),
|
||||
type_code: docTypeLabel["encounter-patient"],
|
||||
name: 'example',
|
||||
filePath: 'https://bing.com',
|
||||
fileName: 'example',
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DeathCause } from "./death-cause"
|
||||
import { type Doctor, genDoctor } from "./doctor"
|
||||
import { genEmployee, type Employee } from "./employee"
|
||||
import type { EncounterDocument } from "./encounter-document"
|
||||
import type { InternalReference } from "./internal-reference"
|
||||
import { type Patient, genPatient } from "./patient"
|
||||
import type { Specialist } from "./specialist"
|
||||
@@ -37,6 +38,7 @@ export interface Encounter {
|
||||
internalReferences?: InternalReference[]
|
||||
deathCause?: DeathCause
|
||||
status_code: string
|
||||
encounterDocuments: EncounterDocument[]
|
||||
}
|
||||
|
||||
export function genEncounter(): Encounter {
|
||||
@@ -54,7 +56,8 @@ export function genEncounter(): Encounter {
|
||||
appointment_doctor_id: 0,
|
||||
appointment_doctor: genDoctor(),
|
||||
medicalDischargeEducation: '',
|
||||
status_code: ''
|
||||
status_code: '',
|
||||
encounterDocuments: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { type Base, genBase } from "./_base"
|
||||
|
||||
export interface MedicineForm extends Base {
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface CreateDto {
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface GetListDto {
|
||||
page: number
|
||||
size: number
|
||||
name?: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
export interface GetDetailDto {
|
||||
id?: string
|
||||
}
|
||||
|
||||
export interface UpdateDto extends CreateDto {
|
||||
id?: number
|
||||
}
|
||||
|
||||
export interface DeleteDto {
|
||||
id?: string
|
||||
}
|
||||
|
||||
export function genMedicine(): MedicineForm {
|
||||
return {
|
||||
...genBase(),
|
||||
name: 'name',
|
||||
code: 'code',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import type { PagePermission } from '~/models/role'
|
||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['rbac'],
|
||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||
title: 'Update Surat Kontrol',
|
||||
contentFrame: 'cf-full-width',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||
|
||||
const { checkRole, hasReadAccess } = useRBAC()
|
||||
|
||||
// Check if user has access to this page
|
||||
const hasAccess = checkRole(roleAccess)
|
||||
// if (!hasAccess) {
|
||||
// navigateTo('/403')
|
||||
// }
|
||||
|
||||
// Define permission-based computed properties
|
||||
// const canRead = hasReadAccess(roleAccess)
|
||||
const canRead = true
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="canRead">
|
||||
<ContentDocumentUploadEdit/>
|
||||
</div>
|
||||
<Error v-else :status-code="403" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import type { PagePermission } from '~/models/role'
|
||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['rbac'],
|
||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||
title: 'Tambah Surat Kontrol',
|
||||
contentFrame: 'cf-full-width',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||
|
||||
const { checkRole, hasReadAccess } = useRBAC()
|
||||
|
||||
// Check if user has access to this page
|
||||
const hasAccess = checkRole(roleAccess)
|
||||
// if (!hasAccess) {
|
||||
// navigateTo('/403')
|
||||
// }
|
||||
|
||||
// Define permission-based computed properties
|
||||
// const canRead = hasReadAccess(roleAccess)
|
||||
const canRead = true
|
||||
const callbackUrl = route.query['return-path'] as string | undefined
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="canRead">
|
||||
<ContentDocumentUploadAdd :callback-url="callbackUrl" />
|
||||
</div>
|
||||
<Error v-else :status-code="403" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,9 +22,9 @@ const { checkRole, hasReadAccess } = useRBAC()
|
||||
|
||||
// Check if user has access to this page
|
||||
const hasAccess = checkRole(roleAccess)
|
||||
if (!hasAccess) {
|
||||
// navigateTo('/403')
|
||||
}
|
||||
// if (!hasAccess) {
|
||||
// navigateTo('/403')
|
||||
// }
|
||||
|
||||
// Define permission-based computed properties
|
||||
const canRead = true // hasReadAccess(roleAccess)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import type { PagePermission } from '~/models/role'
|
||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['rbac'],
|
||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||
title: 'Daftar Dokter',
|
||||
contentFrame: 'cf-container-lg',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor']
|
||||
|
||||
const { checkRole, hasReadAccess } = useRBAC()
|
||||
|
||||
// Check if user has access to this page
|
||||
const hasAccess = checkRole(roleAccess)
|
||||
// if (!hasAccess) {
|
||||
// navigateTo('/403')
|
||||
// }
|
||||
|
||||
// Define permission-based computed properties
|
||||
const canRead = true // hasReadAccess(roleAccess)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="canRead">
|
||||
<ContentMedicineFormList />
|
||||
</template>
|
||||
<Error v-else :status-code="403" />
|
||||
</template>
|
||||
@@ -22,12 +22,12 @@ const { checkRole, hasReadAccess } = useRBAC()
|
||||
|
||||
// Check if user has access to this page
|
||||
const hasAccess = checkRole(roleAccess)
|
||||
if (!hasAccess) {
|
||||
navigateTo('/403')
|
||||
}
|
||||
// if (!hasAccess) {
|
||||
// navigateTo('/403')
|
||||
// }
|
||||
|
||||
// Define permission-based computed properties
|
||||
const canRead = hasReadAccess(roleAccess)
|
||||
const canRead = true // hasReadAccess(roleAccess)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const ACCEPTED_UPLOAD_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
|
||||
const MAX_SIZE_BYTES = 1 * 1024 * 1024 // 1MB
|
||||
|
||||
const DocumentUploadSchema = z.object({
|
||||
entityType_code: z.string().default('encounter'),
|
||||
ref_id: z.number(),
|
||||
upload_employee_id: z.number(),
|
||||
name: z.string({ required_error: 'Mohon isi', }),
|
||||
type_code: z.string({ required_error: 'Mohon isi', }),
|
||||
content: z.custom<File>()
|
||||
.refine((f) => f, { message: 'File tidak boleh kosong' })
|
||||
.refine((f) => !f || f instanceof File, { message: 'Harus berupa file yang valid' })
|
||||
.refine((f) => !f || ACCEPTED_UPLOAD_TYPES.includes(f.type), {
|
||||
message: 'Format file harus JPG, PNG, atau PDF',
|
||||
})
|
||||
.refine((f) => !f || f.size <= MAX_SIZE_BYTES, { message: 'Maksimal 1MB' }),
|
||||
})
|
||||
|
||||
type DocumentUploadFormData = z.infer<typeof DocumentUploadSchema>
|
||||
|
||||
export { DocumentUploadSchema }
|
||||
export type { DocumentUploadFormData }
|
||||
@@ -5,6 +5,7 @@ export const MedicineSchema = z.object({
|
||||
name: z.string({ required_error: 'Nama harus diisi' }).min(1, 'Nama minimal 1 karakter'),
|
||||
medicineGroup_code: z.string({ required_error: 'Kelompok obat harus diisi' }).min(1, 'Kelompok obat harus diisi'),
|
||||
medicineMethod_code: z.string({ required_error: 'Metode pemberian harus diisi' }).min(1, 'Metode pemberian harus diisi'),
|
||||
medicineForm_code: z.string({ required_error: 'Sediaan Obat harus diisi' }).min(1, 'Sediaan Obat harus diisi'),
|
||||
uom_code: z.string({ required_error: 'Satuan harus diisi' }).min(1, 'Satuan harus diisi'),
|
||||
infra_id: z.number().nullable().optional(),
|
||||
stock: z.preprocess((val) => Number(val), z.number({ invalid_type_error: 'Stok harus berupa angka' }).min(1, 'Stok harus lebih besar dari 0')),
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
// Types
|
||||
import type { MedicineForm } from '~/models/medicine-form'
|
||||
|
||||
const path = '/api/v1/medicine-form'
|
||||
const name = 'medicine-form'
|
||||
|
||||
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): 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: MedicineForm) => ({
|
||||
value: item.code,
|
||||
label: item.name,
|
||||
}))
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Base
|
||||
import * as base from './_crud-base'
|
||||
|
||||
// Constants
|
||||
import { encounterClassCodes, uploadCode, type UploadCodeKey } from '~/lib/constants'
|
||||
|
||||
const path = '/api/v1/encounter-document'
|
||||
const create_path = '/api/v1/upload'
|
||||
const name = 'encounter-document'
|
||||
|
||||
export function create(data: any) {
|
||||
return base.create(create_path, data, name)
|
||||
}
|
||||
|
||||
export function getList(params: any = null) {
|
||||
return base.getList(path, params, name)
|
||||
}
|
||||
|
||||
export function getDetail(id: number | string, params?: any) {
|
||||
return base.getDetail(path, id, name, params)
|
||||
}
|
||||
|
||||
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 uploadAttachment(file: File, userId: number, key: UploadCodeKey) {
|
||||
try {
|
||||
const resolvedKey = uploadCode[key]
|
||||
if (!resolvedKey) {
|
||||
throw new Error(`Invalid upload code key: ${key}`)
|
||||
}
|
||||
|
||||
// siapkan form-data body
|
||||
const formData = new FormData()
|
||||
formData.append('code', resolvedKey)
|
||||
formData.append('content', file)
|
||||
|
||||
// kirim via xfetch
|
||||
const resp = await xfetch(`${path}/${userId}/upload`, 'POST', formData)
|
||||
|
||||
// struktur hasil sama seperti patchPatient
|
||||
const result: any = {}
|
||||
result.success = resp.success
|
||||
result.body = (resp.body as Record<string, any>) || {}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Error uploading attachment:', error)
|
||||
throw new Error('Failed to upload attachment')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
[
|
||||
{
|
||||
"heading": "Menu Utama",
|
||||
"items": [
|
||||
{
|
||||
"title": "Dashboard",
|
||||
"icon": "i-lucide-home",
|
||||
"link": "/"
|
||||
},
|
||||
{
|
||||
"title": "Rawat Jalan",
|
||||
"icon": "i-lucide-stethoscope",
|
||||
"children": [
|
||||
{
|
||||
"title": "Antrian Pendaftaran",
|
||||
"link": "/outpatient/registration-queue"
|
||||
},
|
||||
{
|
||||
"title": "Antrian Poliklinik",
|
||||
"link": "/outpatient/polyclinic-queue"
|
||||
},
|
||||
{
|
||||
"title": "Kunjungan",
|
||||
"link": "/outpatient/encounter"
|
||||
},
|
||||
{
|
||||
"title": "Konsultasi",
|
||||
"link": "/outpatient/consultation"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "IGD",
|
||||
"icon": "i-lucide-zap",
|
||||
"children": [
|
||||
{
|
||||
"title": "Triase",
|
||||
"link": "/emergency/triage"
|
||||
},
|
||||
{
|
||||
"title": "Kunjungan",
|
||||
"link": "/emergency/encounter"
|
||||
},
|
||||
{
|
||||
"title": "Konsultasi",
|
||||
"link": "/emergency/consultation"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Rehab Medik",
|
||||
"icon": "i-lucide-bike",
|
||||
"children": [
|
||||
{
|
||||
"title": "Antrean Pendaftaran",
|
||||
"link": "/rehab/registration-queue"
|
||||
},
|
||||
{
|
||||
"title": "Antrean Poliklinik",
|
||||
"link": "/rehab/polyclinic-queue"
|
||||
},
|
||||
{
|
||||
"title": "Kunjungan",
|
||||
"link": "/rehab/encounter"
|
||||
},
|
||||
{
|
||||
"title": "Konsultasi",
|
||||
"link": "/rehab/consultation"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Rawat Inap",
|
||||
"icon": "i-lucide-building-2",
|
||||
"children": [
|
||||
{
|
||||
"title": "Permintaan",
|
||||
"link": "/inpatient/request"
|
||||
},
|
||||
{
|
||||
"title": "Kunjungan",
|
||||
"link": "/inpatient/encounter"
|
||||
},
|
||||
{
|
||||
"title": "Konsultasi",
|
||||
"link": "/inpatient/consultation"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Obat - Order",
|
||||
"icon": "i-lucide-briefcase-medical",
|
||||
"children": [
|
||||
{
|
||||
"title": "Permintaan",
|
||||
"link": "/medication/order"
|
||||
},
|
||||
{
|
||||
"title": "Standing Order",
|
||||
"link": "/medication/standing-order"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Lab - Order",
|
||||
"icon": "i-lucide-microscope",
|
||||
"link": "/pc-lab-order"
|
||||
},
|
||||
{
|
||||
"title": "Lab Mikro - Order",
|
||||
"icon": "i-lucide-microscope",
|
||||
"link": "/micro-lab-order"
|
||||
},
|
||||
{
|
||||
"title": "Lab PA - Order",
|
||||
"icon": "i-lucide-microscope",
|
||||
"link": "/pa-lab-order"
|
||||
},
|
||||
{
|
||||
"title": "Radiologi - Order",
|
||||
"icon": "i-lucide-radio",
|
||||
"link": "/radiology-order"
|
||||
},
|
||||
{
|
||||
"title": "Gizi",
|
||||
"icon": "i-lucide-egg-fried",
|
||||
"link": "/nutrition-order"
|
||||
},
|
||||
{
|
||||
"title": "Pembayaran",
|
||||
"icon": "i-lucide-banknote-arrow-up",
|
||||
"link": "/payment"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "Ruang Tindakan Rajal",
|
||||
"items": [
|
||||
{
|
||||
"title": "Kemoterapi",
|
||||
"icon": "i-lucide-droplets",
|
||||
"link": "/outpation-action/cemotherapy"
|
||||
},
|
||||
{
|
||||
"title": "Hemofilia",
|
||||
"icon": "i-lucide-droplet-off",
|
||||
"link": "/outpation-action/hemophilia"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "Ruang Tindakan Anak",
|
||||
"items": [
|
||||
{
|
||||
"title": "Thalasemi",
|
||||
"icon": "i-lucide-baby",
|
||||
"link": "/children-action/thalasemia"
|
||||
},
|
||||
{
|
||||
"title": "Echocardiography",
|
||||
"icon": "i-lucide-baby",
|
||||
"link": "/children-action/echocardiography"
|
||||
},
|
||||
{
|
||||
"title": "Spirometri",
|
||||
"icon": "i-lucide-baby",
|
||||
"link": "/children-action/spirometry"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "Client",
|
||||
"items": [
|
||||
{
|
||||
"title": "Pasien",
|
||||
"icon": "i-lucide-users",
|
||||
"link": "/client/patient"
|
||||
},
|
||||
{
|
||||
"title": "Rekam Medis",
|
||||
"icon": "i-lucide-file-text",
|
||||
"link": "/client/medical-record"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "Integrasi",
|
||||
"items": [
|
||||
{
|
||||
"title": "BPJS",
|
||||
"icon": "i-lucide-circuit-board",
|
||||
"children": [
|
||||
{
|
||||
"title": "SEP",
|
||||
"icon": "i-lucide-circuit-board",
|
||||
"link": "/integration/bpjs/sep"
|
||||
},
|
||||
{
|
||||
"title": "Peserta",
|
||||
"icon": "i-lucide-circuit-board",
|
||||
"link": "/integration/bpjs/member"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "SATUSEHAT",
|
||||
"icon": "i-lucide-database",
|
||||
"link": "/integration/satusehat"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "Source",
|
||||
"items": [
|
||||
{
|
||||
"title": "Peralatan dan Perlengkapan",
|
||||
"icon": "i-lucide-layout-dashboard",
|
||||
"children": [
|
||||
{
|
||||
"title": "Obat",
|
||||
"link": "/tools-equipment-src/medicine"
|
||||
},
|
||||
{
|
||||
"title": "Peralatan",
|
||||
"link": "/tools-equipment-src/tools"
|
||||
},
|
||||
{
|
||||
"title": "Perlengkapan (BMHP)",
|
||||
"link": "/tools-equipment-src/equipment"
|
||||
},
|
||||
{
|
||||
"title": "Metode Obat",
|
||||
"link": "/tools-equipment-src/medicine-method"
|
||||
},
|
||||
{
|
||||
"title": "Jenis Obat",
|
||||
"link": "/tools-equipment-src/medicine-type"
|
||||
},
|
||||
{
|
||||
"title": "Sediaan Obat",
|
||||
"link": "/tools-equipment-src/medicine-form"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Pengguna",
|
||||
"icon": "i-lucide-user",
|
||||
"children": [
|
||||
{
|
||||
"title": "Pegawai",
|
||||
"link": "/human-src/employee"
|
||||
},
|
||||
{
|
||||
"title": "PPDS",
|
||||
"link": "/human-src/specialist-intern"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Pemeriksaan Penunjang",
|
||||
"icon": "i-lucide-layout-list",
|
||||
"children": [
|
||||
{
|
||||
"title": "Checkup",
|
||||
"link": "/mcu-src/mcu"
|
||||
},
|
||||
{
|
||||
"title": "Prosedur",
|
||||
"link": "/mcu-src/procedure"
|
||||
},
|
||||
{
|
||||
"title": "Diagnosis",
|
||||
"link": "/mcu-src/diagnose"
|
||||
},
|
||||
{
|
||||
"title": "Medical Action",
|
||||
"link": "/mcu-src/medical-action"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Layanan",
|
||||
"icon": "i-lucide-layout-list",
|
||||
"children": [
|
||||
{
|
||||
"title": "Counter",
|
||||
"link": "/service-src/counter"
|
||||
},
|
||||
{
|
||||
"title": "Public Screen (Big Screen)",
|
||||
"link": "/service-src/public-screen"
|
||||
},
|
||||
{
|
||||
"title": "Kasur",
|
||||
"link": "/service-src/bed"
|
||||
},
|
||||
{
|
||||
"title": "Kamar",
|
||||
"link": "/service-src/chamber"
|
||||
},
|
||||
{
|
||||
"title": "Ruang",
|
||||
"link": "/service-src/room"
|
||||
},
|
||||
{
|
||||
"title": "Depo",
|
||||
"link": "/service-src/warehouse"
|
||||
},
|
||||
{
|
||||
"title": "Lantai",
|
||||
"link": "/service-src/floor"
|
||||
},
|
||||
{
|
||||
"title": "Gedung",
|
||||
"link": "/service-src/building"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Organisasi",
|
||||
"icon": "i-lucide-network",
|
||||
"children": [
|
||||
{
|
||||
"title": "Divisi",
|
||||
"link": "/org-src/division"
|
||||
},
|
||||
{
|
||||
"title": "Instalasi",
|
||||
"link": "/org-src/installation"
|
||||
},
|
||||
{
|
||||
"title": "Unit",
|
||||
"link": "/org-src/unit"
|
||||
},
|
||||
{
|
||||
"title": "Spesialis",
|
||||
"link": "/org-src/specialist"
|
||||
},
|
||||
{
|
||||
"title": "Sub Spesialis",
|
||||
"link": "/org-src/subspecialist"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Umum",
|
||||
"icon": "i-lucide-airplay",
|
||||
"children": [
|
||||
{
|
||||
"title": "Uom",
|
||||
"link": "/common/uom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Keuangan",
|
||||
"icon": "i-lucide-airplay",
|
||||
"children": [
|
||||
{
|
||||
"title": "Item & Pricing",
|
||||
"link": "/common/item"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -240,6 +240,10 @@
|
||||
{
|
||||
"title": "Jenis Obat",
|
||||
"link": "/tools-equipment-src/medicine-type"
|
||||
},
|
||||
{
|
||||
"title": "Sediaan Obat",
|
||||
"link": "/tools-equipment-src/medicine-form"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user