Merge branch 'dev' of github.com:dikstub-rssa/simrs-fe into feat/patient-63-adjustment

This commit is contained in:
Khafid Prayoga
2025-12-08 15:37:35 +07:00
165 changed files with 8528 additions and 1005 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,468 @@
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { type Duration, intervalToDuration } from 'date-fns'
// schema
import { type ActionReportFormData, ActionReportSchema } from '~/schemas/action-report.schema'
// type
import type { Doctor } from '~/models/doctor'
import { type ClickType as ActionClickType } from '~/components/pub/my-ui/nav-footer/index'
// components
import * as DE from '~/components/pub/my-ui/doc-entry'
import Separator from '~/components/pub/ui/separator/Separator.vue'
import { ArrayMessage } from '~/components/pub/ui/form'
import ActionForm from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
// form field components
import {
FillNotes,
RadioBloods,
SelectBilling,
SelectBirthPlace,
SelectBirthType,
SelectOperationSystem,
SelectOperationType,
SelectSpecimen,
SelectSurgeryCounter,
SelectSurgeryType,
} from './fields'
import { ButtonAction, Fragment, InputBase, TextAreaInput } from '~/components/pub/my-ui/form/'
import { SelectDoctor } from '~/components/app/doctor/fields'
// Helpers
// #region Props & Emits
interface FormData extends ActionReportFormData {
_operationDuration: string
_anesthesiaDuration: string
}
interface Props {
isLoading: boolean
mode: 'add' | 'edit' | 'view'
initialValues?: Partial<FormData>
// form related
doctors: Doctor[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'submit', payload: FormData): void
(e: 'back'): void
(e: 'error', errors: Error): void
}>()
const tissueNotesLimit = 5
const mode = toRef(props, 'mode')
const isLoading = toRef(props, 'isLoading')
const isReadonly = computed(() => {
if (isLoading.value === true) {
return true
}
if (mode.value === 'view') {
return true
}
return false
})
const formSchema = toTypedSchema(ActionReportSchema)
const { errors, handleSubmit, values, meta, resetForm, setFieldValue, setValues, validate } = useForm<FormData>({
name: 'encounterActionReportForm',
validationSchema: formSchema,
initialValues: props.initialValues ? props.initialValues : {},
validateOnMount: false,
})
defineExpose({
validate,
resetForm,
setValues,
values,
})
// #endregion
// #region State & Computed
// #endregion
// #region Lifecycle Hooks
// #endregion
// #region Functions
// #endregion region
// #region Utilities & event handlers
const onFormActionClicked = (action: ActionClickType) => {
if (action === 'back') {
emit('back')
return
}
if (action === 'submit') {
onSubmit()
return
}
}
const onSubmit = handleSubmit(
(values) => {
console.log(JSON.stringify(values))
emit('submit', values)
},
(errors) => {
console.error(errors)
emit('error', new Error('Silahkan lengkapi form terlebih dahulu'))
},
)
// #endregion
// #region Watcher
watch(
() => [values.operationExecution.operationStartAt, values.operationExecution.operationEndAt],
([start, end]) => {
if (!start || !end) return
const pStart = new Date(start)
const pEnd = new Date(end)
const formatTime = (r: Duration) =>
[r.hours && `${r.hours} jam`, r.minutes && `${r.minutes} menit`, r.seconds && `${r.seconds} detik`]
.filter(Boolean)
.join(' ')
const res = intervalToDuration({
start: pStart,
end: pEnd,
})
setFieldValue('_operationDuration', formatTime(res))
},
{ immediate: true },
)
watch(
() => [values.operationExecution.anesthesiaStartAt, values.operationExecution.anesthesiaEndAt],
([start, end]) => {
if (!start || !end) return
const pStart = new Date(start)
const pEnd = new Date(end)
const formatTime = (r: Duration) =>
[r.hours && `${r.hours} jam`, r.minutes && `${r.minutes} menit`, r.seconds && `${r.seconds} detik`]
.filter(Boolean)
.join(' ')
const res = intervalToDuration({
start: pStart,
end: pEnd,
})
setFieldValue('_anesthesiaDuration', formatTime(res))
},
{ immediate: true },
)
// #endregion
</script>
<template>
<form @submit.prevent="onSubmit">
<Fragment
v-slot="{ section }"
title="Tim Pelaksana Tindakan"
>
<p class="text-lg font-semibold">{{ section }}</p>
<DE.Block
:col-count="4"
:cell-flex="false"
>
<SelectDoctor
fieldName="operatorTeam.dpjpId"
label="Dokter Pemeriksa"
placeholder="Pilih dokter"
:doctors="doctors"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operatorTeam.operatorName"
label="Operator"
placeholder="Masukkan operator"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operatorTeam.assistantOperatorName"
label="Asisten Operator"
placeholder="Masukkan asisten operator"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operatorTeam.instrumentNurseName"
label="Instrumentir"
placeholder="Masukkan instrumentir"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operatorTeam.surgeryDate"
label="Tanggal Pembedahan"
input-type="datetime-local"
:is-disabled="isReadonly"
placeholder=""
/>
</DE.Block>
<DE.Block
:col-count="4"
:cell-flex="false"
>
<TextAreaInput
field-name="operatorTeam.actionDiagnosis"
label="Diagnosa Tindakan"
placeholder="Masukkan diagnosa tindakan"
:col-span="2"
:rows="5"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operatorTeam.postSurgeryNurseId"
label="Perawat Pasca Bedah"
placeholder="Masukkan perawat pasca bedah"
:is-disabled="isReadonly"
/>
</DE.Block>
</Fragment>
<Separator class="my-4" />
<Fragment
v-slot="{ section }"
title="Tindakan Operatif/Non Operatif Lain"
>
<!-- <p class="text-lg font-semibold">{{ section }}</p> -->
<DE.Block
:col-count="2"
:cell-flex="false"
>
<DE.Cell>
<slot name="procedures" />
<ArrayMessage
class="mt-1"
v-if="meta.touched"
name="procedures"
/>
</DE.Cell>
</DE.Block>
</Fragment>
<Separator class="my-4" />
<Fragment
v-slot="{ section }"
title="Data Pelaksanaan Operasi"
>
<p class="text-lg font-semibold">{{ section }}</p>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<SelectSurgeryType
field-name="operationExecution.surgeryType"
label="Jenis Operasi"
placeholder="Pilih jenis operasi"
:is-disabled="isReadonly"
/>
<SelectBilling
field-name="operationExecution.billingCode"
label="Kode Billing"
placeholder="Pilih kode billing"
:is-disabled="isReadonly"
/>
<SelectOperationSystem
field-name="operationExecution.operationSystem"
label="Sistem Operasi"
placeholder="Pilih sistem operasi"
:is-disabled="isReadonly"
/>
</DE.Block>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<InputBase
field-name="operationExecution.operationStartAt"
label="Operasi Mulai"
placeholder="Pilih Tanggal"
input-type="datetime-local"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operationExecution.operationEndAt"
label="Operasi Selesai"
placeholder="Pilih Tanggal"
input-type="datetime-local"
:is-disabled="isReadonly"
/>
<InputBase
field-name="_operationDuration"
label="Lama Operasi"
placeholder="-"
is-disabled
/>
</DE.Block>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<InputBase
field-name="operationExecution.anesthesiaStartAt"
label="Pembiusan Mulai"
placeholder="Pilih Tanggal"
input-type="datetime-local"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operationExecution.anesthesiaEndAt"
label="Pembiusan Selesai"
placeholder="Pilih Tanggal"
input-type="datetime-local"
:is-disabled="isReadonly"
/>
<InputBase
field-name="_anesthesiaDuration"
label="Lama Pembiusan"
placeholder="-"
is-disabled
/>
</DE.Block>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<SelectOperationType
field-name="operationExecution.surgeryCleanType"
label="Jenis Pembedahan"
placeholder="Pilih jenis pembedahan"
:is-disabled="isReadonly"
/>
<SelectSurgeryCounter
field-name="operationExecution.surgeryNumber"
label="Operasi Ke"
placeholder="Pilih"
:is-disabled="isReadonly"
/>
<SelectBirthType
field-name="operationExecution.birthRemark"
label="Keterangan Lahir"
placeholder="Pilih"
:is-disabled="isReadonly"
/>
<SelectBirthPlace
field-name="operationExecution.birthPlaceNote"
label="Ket. Tempat Lahir"
placeholder="Pilih keterangan tempat lahir"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operationExecution.personWeight"
label="Berat Badan"
placeholder="Masukkan berat badan"
numeric-only
suffix-msg="Gram"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operationExecution.birthCondition"
label="Ket. Saat Lahir"
placeholder="Tambah catatan"
:is-disabled="isReadonly"
/>
</DE.Block>
<DE.Block
:col-count="4"
:cell-flex="false"
>
<TextAreaInput
field-name="operationExecution.operationDescription"
label="Uraian Operasi"
placeholder="Masukkan uraian"
:rows="3"
:col-span="2"
:is-disabled="isReadonly"
/>
<InputBase
field-name="operationExecution.bleedingAmountCc"
label="Jumlah Pendarahan"
placeholder="Masukkan jumlah pendarahan"
suffix-msg="CC"
numeric-only
:is-disabled="isReadonly"
/>
</DE.Block>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<RadioBloods
field-name="bloodInput"
label="Jenis & Jumlah Darah Masuk"
:is-disabled="isReadonly"
/>
</DE.Block>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<InputBase
field-name="implant.brand"
label="Merk"
placeholder="Masukkan merk"
:is-disabled="isReadonly"
/>
<InputBase
field-name="implant.name"
label="Nama Implant"
placeholder="Masukkan nama implant"
:is-disabled="isReadonly"
/>
<InputBase
field-name="implant.sticker"
label="Sticker/Nomer Register Implant"
placeholder="Masukkan sticker/nomor register implant"
:is-disabled="isReadonly"
/>
<InputBase
field-name="implant.companionName"
label="Nama Pendamping Implant"
placeholder="Masukkan nama pendamping implant"
:is-disabled="isReadonly"
/>
<SelectSpecimen
field-name="specimen.destination"
label="Specimen/Jaringan dikirim ke"
placeholder="Pilih"
:is-disabled="isReadonly"
/>
</DE.Block>
<FillNotes
title="Keterangan Jaringan"
:limit="tissueNotesLimit"
:is-disabled="isReadonly"
/>
</Fragment>
<div class="mt-4 flex justify-end">
<ActionForm @click="onFormActionClicked" />
</div>
</form>
</template>
@@ -0,0 +1,65 @@
<script setup lang="ts">
// components
import { FieldArray } from 'vee-validate'
import * as DE from '~/components/pub/my-ui/doc-entry'
// form field components
import { ButtonAction, InputBase } from '~/components/pub/my-ui/form'
interface Props {
isDisabled: boolean
limit: number
title: string
}
const props = defineProps<Props>()
const isReadonly = computed(() => props.isDisabled)
</script>
<template>
<FieldArray
v-slot="{ fields, push, remove }"
name="tissueNotes"
>
<template v-if="fields.length === 0">
{{ push({ note: '' }) }}
</template>
<div class="space-y-4">
<DE.Block
v-for="(field, idx) in fields"
:key="field.key"
:col-count="3"
:cell-flex="false"
>
<InputBase
:label="idx === 0 ? 'Keterangan Jaringan' : undefined"
:field-name="`tissueNotes[${idx}].note`"
placeholder="Masukkan catatan"
:is-disabled="isReadonly"
/>
<DE.Cell class="flex items-start justify-start">
<DE.Field :class="idx === 0 ? 'mt-[30px]' : 'mt-0'">
<ButtonAction
v-if="idx !== 0"
:disabled="isReadonly"
preset="delete"
:title="`Hapus Kontak ${idx + 1}`"
icon-only
@click="remove(idx)"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
</div>
<ButtonAction
preset="add"
label="Tambah Catatan"
title="Tambah Catatan Keterangan Jaringan"
:disabled="fields.length >= limit || isReadonly"
:full-width-mobile="true"
class="mt-4"
@click="push({ name: '', dose: '', unit: '' })"
/>
</FieldArray>
</template>
@@ -0,0 +1,10 @@
export { default as FillNotes } from './fill-notes.vue'
export { default as RadioBloods } from './radio-bloods.vue'
export { default as SelectBilling } from './select-billing.vue'
export { default as SelectBirthPlace } from './select-birth-place.vue'
export { default as SelectBirthType } from './select-birth-type.vue'
export { default as SelectOperationSystem } from './select-operation-system.vue'
export { default as SelectOperationType } from './select-operation-type.vue'
export { default as SelectSpecimen } from './select-specimen.vue'
export { default as SelectSurgeryCounter } from './select-surgery-counter.vue'
export { default as SelectSurgeryType } from './select-surgery-type.vue'
@@ -0,0 +1,101 @@
<script setup lang="ts">
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
import { Label as RadioLabel } from '~/components/pub/ui/label'
import { Input } from '~/components/pub/ui/input'
const props = defineProps<{
fieldName: string
label: string
class?: string
radioGroupClass?: string
radioItemClass?: string
labelClass?: string
isDisabled?: boolean
}>()
const { class: containerClass, radioGroupClass, radioItemClass, labelClass } = props
const opts = [
{ label: 'PRC', value: 'prc' },
{ label: 'WB', value: 'wb' },
{ label: 'FFP', value: 'ffp' },
{ label: 'TC', value: 'tc' },
]
</script>
<template>
<DE.Cell
:class="cn('radio-group-field', containerClass)"
:col-span="2"
>
<DE.Label :label-for="fieldName">
{{ label }}
</DE.Label>
<DE.Field :id="fieldName">
<FormField
v-slot="{ componentField: radioField }"
:name="`${fieldName}.type`"
>
<FormItem>
<FormControl>
<RadioGroup
:model-value="radioField.modelValue"
@update:model-value="radioField.onChange"
:class="cn('grid grid-cols-1 items-start gap-4 sm:grid-cols-2 ', radioGroupClass)"
>
<div
v-for="(option, index) in opts"
:key="option.value"
:class="cn('grid grid-cols-1 items-start gap-3 sm:grid-cols-[auto,1fr]', radioItemClass)"
>
<div class="flex min-w-fit items-center gap-2">
<RadioGroupItem
:id="`type-${index}`"
:value="option.value"
:disabled="isDisabled"
/>
<RadioLabel :for="`type-${index}`">
{{ option.label }}
</RadioLabel>
</div>
<FormField
v-slot="{ componentField: amountField }"
:name="`${fieldName}.amount.${option.value}`"
>
<FormItem>
<FormControl>
<div class="relative w-[140px]">
<Input
v-bind="amountField"
placeholder="00"
class="pr-10"
:disabled="radioField.modelValue !== option.value || isDisabled"
@input="
(e: InputEvent) => {
const target = e.target as HTMLInputElement
const v = target.value.replace(/\D/g, '')
amountField.onChange(v)
}
"
/>
<span
class="absolute inset-y-0 end-0 flex items-center justify-center px-2 text-sm text-muted-foreground"
>
CC
</span>
</div>
</FormControl>
</FormItem>
</FormField>
</div>
</RadioGroup>
</FormControl>
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,68 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'General', value: 'general' },
{ label: 'Regional', value: 'regional' },
{ label: 'Local', value: 'local' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
: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>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'RSSA', value: 'rssa' },
{ label: 'Bidan Luar', value: 'out1' },
{ label: 'Dokter Luar', value: 'out2' },
{ label: 'Dukun Bayi', value: 'out3' },
{ label: 'Puskesmas', value: 'out4' },
{ label: 'Paramedis Luar', value: 'out5' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
: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>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,67 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'Lahir Hidup', value: 'lahir_hidup' },
{ label: 'Lahir Mati', value: 'lahir_mati' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
: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>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'Cito', value: 'cito' },
{ label: 'Urgent', value: 'urgent' },
{ label: 'Efektif', value: 'efektif' },
{ label: 'Khusus', value: 'khusus' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
: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>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'Bersih', value: 'bersih' },
{ label: 'Bersih Terkontaminasi', value: 'bersih_terkontaminasi' },
{ label: 'Terkontaminasi Kotor', value: 'terkontaminasi' },
{ label: 'Kotor', value: 'kotor' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
: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>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'PA', value: 'pa' },
{ label: 'Mikrobiologi', value: 'microbiology' },
{ label: 'Laborat', value: 'laboratory' },
{ label: 'Tidak Perlu', value: 'none' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
: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>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,67 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: '1 (Satu)', value: 'first' },
{ label: 'Ulangan', value: 'retry' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
: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>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
isDisabled?: boolean
isRequired?: boolean
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
}>()
const { errors, class: containerClass, selectClass, fieldGroupClass } = props
const opts = [
{ label: 'Kecil', value: 'kecil' },
{ label: 'Sedang', value: 'sedang' },
{ label: 'Besar', value: 'besar' },
{ label: 'Khusus', value: 'khusus' },
]
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
label-for="fieldName"
: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>
<Select
:id="fieldName"
:is-disabled="isDisabled"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,88 @@
import { defineAsyncComponent } from 'vue'
import { format } from 'date-fns'
import { id } from 'date-fns/locale'
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import type { ActionReportData } from '~/components/app/action-report/sample'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-d.vue'))
export const config: Config = {
cols: [
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 50 },
],
headers: [
[
{ label: 'TANGGAL LAPORAN' },
{ label: 'DPJP' },
{ label: 'OPERATOR' },
{ label: 'TANGGAL PEMBEDAHAN' },
{ label: 'JENIS OPERASI' },
{ label: 'KODE BILLING' },
{ label: 'SISTEM OPERASI' },
{ label: 'AKSI' },
],
],
keys: ['reportAt', 'dpjp', 'operator', 'operationAt', 'operationType', 'billing', 'system', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
reportAt: (rec: unknown): unknown => {
const attr = (rec as ActionReportData).reportAt
const result = format(new Date(attr), 'd MMMM yyyy, HH:mm', { locale: id })
return result
},
operationAt: (rec: unknown): unknown => {
const attr = (rec as ActionReportData).operationAt
const result = format(new Date(attr), 'd MMMM yyyy', { locale: id })
return result
},
system: (rec: unknown): unknown => {
return 'Cito'
},
operator: (rec: unknown): unknown => {
return 'dr. Dewi Arum Sawitri, Sp.An'
},
billing: (rec: unknown): unknown => {
return 'General'
},
operationType: (rec: unknown): unknown => {
return 'Besar'
},
dpjp: (rec: unknown): unknown => {
return 'dr. Irwansyah Kurniawan Sp.Bo'
},
parent: (rec: unknown): unknown => {
const recX = rec as any
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
@@ -0,0 +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'
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,89 @@
import { defineAsyncComponent } from 'vue'
import { format } from 'date-fns'
import { id } from 'date-fns/locale'
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import type { ActionReportData } from '~/components/app/action-report/sample'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 50 },
],
headers: [
[
{ label: 'TANGGAL LAPORAN' },
{ label: 'DPJP' },
{ label: 'OPERATOR' },
{ label: 'TANGGAL PEMBEDAHAN' },
{ label: 'JENIS OPERASI' },
{ label: 'KODE BILLING' },
{ label: 'SISTEM OPERASI' },
{ label: 'AKSI' },
],
],
keys: ['reportAt', 'dpjp', 'operator', 'operationAt', 'operationType', 'billing', 'system', 'action'],
delKeyNames: [
{ key: 'id', label: 'ID' },
{ key: 'dokter', label: 'Dokter' },
{ key: 'reportAt', label: 'Tanggal Laporan' },
],
parses: {
reportAt: (rec: unknown): unknown => {
const attr = (rec as ActionReportData).reportAt
const result = format(new Date(attr), 'd MMMM yyyy, HH:mm', { locale: id })
return result
},
operationAt: (rec: unknown): unknown => {
const attr = (rec as ActionReportData).operationAt
const result = format(new Date(attr), 'd MMMM yyyy', { locale: id })
return result
},
system: (rec: unknown): unknown => {
return 'Cito'
},
operator: (rec: unknown): unknown => {
return 'dr. Dewi Arum Sawitri, Sp.An'
},
billing: (rec: unknown): unknown => {
return 'General'
},
operationType: (rec: unknown): unknown => {
return 'Besar'
},
dpjp: (rec: unknown): unknown => {
return 'dr. Irwansyah Kurniawan Sp.Bo'
},
parent: (rec: unknown): unknown => {
const recX = rec as any
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
+39
View File
@@ -0,0 +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'
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,166 @@
<script setup lang="ts">
import { format } from 'date-fns'
import { id } from 'date-fns/locale'
// type
import { type ProcedureSrc } from '~/models/procedure-src'
import { type ActionReportFormData } from '~/schemas/action-report.schema'
// componenets
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '~/components/pub/ui/accordion'
import * as DE from '~/components/pub/my-ui/doc-entry'
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
import ArrangementProcedurePicker from '~/components/app/therapy-protocol/picker-dialog/arrangement-procedure/procedure-picker.vue'
// #region Props & Emits
const props = defineProps<{
data: ActionReportFormData
}>()
const emit = defineEmits<{
(e: 'back'): void
(e: 'edit'): void
}>()
// #endregion
// #region State & Computed
const { operatorTeam, procedures, operationExecution, bloodInput, implant, specimen, tissueNotes = [] } = props.data
const procedureSampleData = procedures as unknown as ProcedureSrc[]
// #region Lifecycle Hooks
// #endregion
// #region Functions
// #endregion region
// #region Utilities & event handlers
function onNavigate(type: string) {
if (type == 'back') emit('back')
if (type == 'edit') emit('edit')
}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<DetailRow label="Tanggal Laporan">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<Accordion
type="multiple"
class="w-full"
collapsible
:default-value="['section-1', 'section-2', 'section-3']"
>
<AccordionItem value="section-1">
<AccordionTrigger>Tim Pelaksanaan Tindakan</AccordionTrigger>
<AccordionContent>
<DE.Block
:cell-flex="false"
:col-count="2"
>
<DE.Cell>
<DetailRow label="DPJP">dr. Marcell Galliard Sp.Gr</DetailRow>
<DetailRow label="Operator">Sumitro</DetailRow>
<DetailRow label="Asisten Operator">Alexis Lewis Carol</DetailRow>
<DetailRow label="Instrumentir">Mikel Arteta</DetailRow>
<DetailRow label="Tanggal Pembedahan">
{{ format(new Date(), 'd MMMM yyyy', { locale: id }) }}
</DetailRow>
<DetailRow label="Diagnosa Tindakan">{{ operatorTeam?.actionDiagnosis || '-' }}</DetailRow>
<DetailRow label="Perawat Pasca Bedah">Cak Armuji</DetailRow>
</DE.Cell>
</DE.Block>
</AccordionContent>
</AccordionItem>
<AccordionItem value="section-2">
<AccordionTrigger>Tindakan Operatif / Non Operatif Lain</AccordionTrigger>
<AccordionContent>
<DE.Block
:cell-flex="false"
:col-count="2"
>
<DE.Cell>
<ArrangementProcedurePicker
field-name="procedures"
title="List Prosedur"
sub-title="Pilih Prosedur"
:mode="'preview'"
:sample-items="procedureSampleData"
/>
</DE.Cell>
</DE.Block>
</AccordionContent>
</AccordionItem>
<AccordionItem value="section-3">
<AccordionTrigger>Data Pelaksanaan Tindakan</AccordionTrigger>
<AccordionContent>
<DE.Block
:cell-flex="false"
:col-count="2"
>
<DE.Cell>
<DetailRow label="Jenis Operasi">dr. Marcell Galliard Sp.Gr</DetailRow>
<DetailRow label="Kode Billing">GCASH1128190</DetailRow>
<DetailRow label="Sistem Operasi">Alexis Lewis Carol</DetailRow>
<DetailRow label="Operasi Mulai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Operasi Selesai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Lama Operasi">5 menit</DetailRow>
<DetailRow label="Pembiusan Mulai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Pembiusan Selesai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Lama Pembiusan">5 menit</DetailRow>
<DetailRow label="PRC">300 CC</DetailRow>
<DetailRow label="FPP">-</DetailRow>
<DetailRow label="WB">-</DetailRow>
<DetailRow label="TC">-</DetailRow>
<DetailRow label="Merk">-</DetailRow>
<DetailRow label="Nama Implant">-</DetailRow>
<DetailRow label="Sticker / Nomor Register Implant">-</DetailRow>
<DetailRow label="Nama Pendamping Implant">-</DetailRow>
</DE.Cell>
<DE.Cell>
<DetailRow label="Jenis Pembedahan">Bersih</DetailRow>
<DetailRow label="Operasi ke">1 (Satu)</DetailRow>
<DetailRow label="Keterangan Lahir">Lahir Hidup</DetailRow>
<DetailRow label="Ket. Tempat Lahir">RSSA</DetailRow>
<DetailRow label="Berat Badan">18 gram</DetailRow>
<DetailRow label="Ket. Saat Lahir">Normal dan sehat</DetailRow>
<DetailRow label="Uraian Operasi">-</DetailRow>
<DetailRow label="Jumlah Pendarahan">300 CC</DetailRow>
<DetailRow label="Specimen / Jaringan dikirim ke">PA</DetailRow>
<DetailRow label="Keterangan Jaringan">
<ul
class="list-disc space-y-1 pl-5 text-sm"
v-if="tissueNotes.length > 0"
v-for="item in tissueNotes"
>
<li>{{ item.note }}</li>
</ul>
<span v-else>-</span>
</DetailRow>
</DE.Cell>
</DE.Block>
</AccordionContent>
</AccordionItem>
</Accordion>
<div class="my-2 flex justify-end py-2">
<PubMyUiNavFooterBaEd @click="onNavigate" />
</div>
</template>
<style scoped></style>
@@ -0,0 +1,54 @@
import { addWeeks, formatISO } from 'date-fns'
export type ActionReportData = {
id: number
reportAt: string
operationAt: string
noRm: string
noBill: string
nama: string
jk: string
alamat: string
klinik: string
dokter: string
caraBayar: string
rujukan: string
ketRujukan: string
asal: string
}
export const sampleRows: ActionReportData[] = [
{
id: 1,
reportAt: formatISO(addWeeks(new Date(), -1)),
operationAt: formatISO(addWeeks(new Date(), 1)),
noRm: 'RM23311224',
noBill: '-',
nama: 'Ahmad Baidowi',
jk: 'L',
alamat: 'Jl Jaksa Agung S. No. 9',
klinik: 'Penyakit dalam',
dokter: 'Dr. Andreas Sutaji',
caraBayar: 'JKN',
rujukan: 'Faskes BPJS',
ketRujukan: 'RUMAH SAKIT - RS Lawang Medika - Malang',
asal: 'Rawat Jalan Reguler',
},
{
id: 2,
reportAt: new Date().toISOString(),
operationAt: formatISO(addWeeks(new Date(), 2)),
noRm: 'RM23455667',
noBill: '-',
nama: 'Abraham Sulaiman',
jk: 'L',
alamat: 'Purwantoro, Blimbing',
klinik: 'Penyakit dalam',
dokter: 'Dr. Andreas Sutaji',
caraBayar: 'JKN',
rujukan: 'Faskes BPJS',
ketRujukan: 'RUMAH SAKIT - RS Lawang Medika - Malang',
asal: 'Rawat Jalan Reguler',
},
// tambahkan lebih banyak baris contoh jika perlu
]
@@ -0,0 +1,58 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [{}, {}, {}, { width: 50 }],
headers: [[
{ label: 'Tgl. Order' },
{ label: 'No. Order' },
{ label: 'Jadwal Pemeriksaan' },
{ label: 'Lokalisasi' },
{ label: 'Stadium' },
{ label: 'Status' },
{ label: 'Resume' },
{ label: '' }]],
keys: [
'date',
'number',
'examinationDate',
'localization',
'stadium',
'resume',
'',
],
delKeyNames: [
{ key: 'date', label: 'Tanggal' },
{ key: 'number', label: 'Nomor' },
],
parses: {
parent: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
props: {
size: 'sm',
},
}
return res
},
},
htmls: {},
}
+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
// Components
import DataTable from '~/components/pub/my-ui/data-table/data-table.vue'
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list.cfg'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<DataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -0,0 +1,215 @@
<script setup lang="ts">
import { z } from 'zod'
import { FieldArray, useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
// types
// import { type AssessmentEducationFormData, AssessmentEducationSchema, encode } from '~/schemas/assessment-education'
// componenets
import * as DE from '~/components/pub/my-ui/doc-entry'
import Separator from '~/components/pub/ui/separator/Separator.vue'
import { BaseSelect, CheckboxGeneral, CheckboxSpecial } from './fields'
import { ButtonAction, TextAreaInput } from '~/components/pub/my-ui/form'
import ActionForm from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
// constant
import {
abilityCode,
willCode,
medObstacleCode,
learnMethodCode,
langClassCode,
translatorSrcCode,
} from '~/lib/clinical.constants'
interface FormData {}
interface Props {
noteLimit?: number
isLoading?: boolean
isReadonly?: boolean
mode: 'add' | 'edit' | 'view'
initialValues?: Partial<FormData>
}
const props = withDefaults(defineProps<Props>(), {
isLoading: false,
isReadonly: false,
noteLimit: 10,
})
const isDisabled = computed(() => props.isLoading || props.isReadonly)
// const formSchema = toTypedSchema(AssessmentEducationSchema)
const formSchema = toTypedSchema(z.object({}))
const { errors, handleSubmit, values, meta, resetForm, setFieldValue, setValues, validate } = useForm<FormData>({
name: 'assessmentEducationForm',
validationSchema: formSchema,
initialValues: props.initialValues
? props.initialValues
: {
plans: [
{
id: 1,
value: '',
},
],
},
validateOnMount: false,
})
defineExpose({
validate,
resetForm,
setValues,
values,
})
</script>
<template>
<form @submit.prevent="">
<p class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base">Kebutuhan Edukasi</p>
<DE.Block
:col-count="4"
:cell-flex="false"
>
<CheckboxGeneral
field-name="generalEducationNeeds"
label="Informasi Umum"
:col-span="2"
:is-disabled="isDisabled"
/>
<CheckboxSpecial
field-name="specificEducationNeeds"
label="Edukasi Khusus"
:col-span="2"
:is-disabled="isDisabled"
/>
</DE.Block>
<div class="h-6">
<Separator />
</div>
<p class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base">Asesmen Kemampuan dan Kemauan Belajar</p>
<DE.Block
:col-count="3"
:cell-flex="false"
>
<BaseSelect
field-name="learningAbility"
label="Kemampuan Belajar"
:items="abilityCode"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="learningWillingness"
label="Kemauan Belajar"
:items="willCode"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="barrier"
label="Hambatan"
:items="medObstacleCode"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="learningMethod"
label="Metode Pembelajaran"
:items="learnMethodCode"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="language"
label="Bahasa"
:items="langClassCode"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="languageBarrier"
label="Hambatan Bahasa"
:items="translatorSrcCode"
:is-disabled="isDisabled"
/>
<BaseSelect
field-name="beliefValue"
label="Keyakinan pada Nilai-Nilai yang Dianut"
:items="{
ya: 'IYA',
tidak: 'TIDAK',
}"
:is-disabled="isDisabled"
/>
</DE.Block>
<div class="h-6">
<Separator />
</div>
<p class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base">Rencana Edukasi</p>
<DE.Block
:col-count="1"
:cell-flex="false"
>
<FieldArray
v-slot="{ fields, push, remove }"
name="tissueNotes"
>
<template v-if="fields.length === 0">
{{ push({ note: '' }) }}
</template>
<div class="space-y-4">
<DE.Block
v-for="(field, idx) in fields"
:key="field.key"
:col-count="6"
:cell-flex="false"
>
<DE.Cell :col-span="5">
<TextAreaInput
:field-name="`plans[${idx}].value`"
:label="'Rencana ' + Number(idx + 1)"
placeholder="Masukkan rencana catatan"
:col-span="2"
:rows="5"
:is-disabled="isReadonly"
/>
</DE.Cell>
<DE.Cell class="flex items-start justify-start">
<DE.Field :class="idx === 0 ? 'mt-[30px]' : 'mt-0'">
<ButtonAction
v-if="idx !== 0"
:disabled="isReadonly"
preset="delete"
:title="`Hapus Rencana ${idx + 1}`"
icon-only
@click="remove(idx)"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
</div>
<div class="self-center pt-3">
<ButtonAction
preset="add"
label="Tambah Rencana"
title="Tambah Rencana Edukasi"
:disabled="fields.length >= noteLimit || isReadonly"
:full-width-mobile="true"
class="mt-4"
@click="push({ id: fields.length + 1, value: '' })"
/>
</div>
</FieldArray>
</DE.Block>
<!-- todo -->
<div class="mt-4 flex justify-end">
<ActionForm @click="() => {}" />
</div>
</form>
</template>
@@ -0,0 +1,66 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
import { Checkbox } from '~/components/pub/ui/checkbox'
import type { CheckItem } from '~/lib/utils'
const props = defineProps<{
label: string
fieldName: string
items: CheckItem[]
isDisabled?: boolean
colSpan?: number
}>()
const { label, fieldName, items } = props
</script>
<template>
<DE.Cell :col-span="colSpan || 1">
<DE.Label :label-for="fieldName">
{{ label }}
</DE.Label>
<DE.Field :id="fieldName">
<FormField
:name="fieldName"
v-slot="{ value, handleChange }"
>
<FormItem>
<div
v-for="item in items"
:key="item.id"
class="ml-1 flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
:disabled="isDisabled"
:id="`cb-${fieldName}-${item.id}`"
:checked="value?.includes(item.id)"
@update:checked="
(checked) => {
const newValue = [...(value || [])]
if (checked) {
if (!newValue.includes(item.id)) newValue.push(item.id)
} else {
const idx = newValue.indexOf(item.id)
if (idx > -1) newValue.splice(idx, 1)
}
handleChange(newValue)
}
"
/>
</FormControl>
<FormLabel
:for="`cb-${fieldName}-${item.id}`"
class="font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 md:!text-xs 2xl:text-sm"
>
{{ item.label }}
</FormLabel>
</div>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,79 @@
<script setup lang="ts">
import Select from '~/components/pub/my-ui/form/select.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName: string
label: string
items: Record<string, string>
placeholder?: string
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const { placeholder = 'Pilih', class: containerClass, selectClass, fieldGroupClass, labelClass } = props
const opts = computed(() => {
const data = mapToComboboxOptList(props.items)
// hide code on select options
const filtered = data
.filter((item) => item.code)
.reduce(
(acc, item) => {
acc.push({
label: item.label as string,
value: item.value as string,
})
return acc
},
[] as Array<{ label: string; value: string }>,
)
return filtered
})
</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"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Select
:id="fieldName"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:preserve-order="false"
:class="
cn(
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
selectClass,
)
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,25 @@
<script setup lang="ts">
import BaseCheckbox from './base-checkbox.vue'
import { generalEduCode } from '~/lib/clinical.constants'
import { mapToCheckItems } from '~/lib/utils'
interface Props {
label: string
fieldName: string
isDisabled?: boolean
colSpan?: number
}
defineProps<Props>()
const generalItems = mapToCheckItems(generalEduCode)
</script>
<template>
<BaseCheckbox
:field-name="fieldName"
:label="label"
:is-disabled="isDisabled"
:col-span="colSpan"
:items="generalItems"
/>
</template>
@@ -0,0 +1,22 @@
<script setup lang="ts">
import BaseCheckbox from './base-checkbox.vue'
import { specialEduCode } from '~/lib/clinical.constants'
import { mapToCheckItems } from '~/lib/utils'
interface Props {
label: string
fieldName: string
colSpan?: number
}
defineProps<Props>()
const specialItems = mapToCheckItems(specialEduCode)
</script>
<template>
<BaseCheckbox
:field-name="fieldName"
:label="label"
:col-span="colSpan"
:items="specialItems"
/>
</template>
@@ -0,0 +1,4 @@
export { default as BaseSelect } from './base-select.vue'
export { default as CheckboxGeneral } from './checkbox-general.vue'
export { default as CheckboxSpecial } from './checkbox-special.vue'
export { default as SelectAssessmentCode } from './select-assessment-code.vue'
@@ -0,0 +1,36 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
label: string
fieldName: string
placeholder?: string
colSpan?: number
}>()
const { label, fieldName, placeholder = 'Masukkan catatan' } = props
</script>
<template>
<DE.Cell :col-span="colSpan || 1">
<DE.Label :label-for="fieldName">
{{ label }}
</DE.Label>
<DE.Field :id="fieldName">
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Textarea
v-bind="componentField"
:placeholder="placeholder"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,75 @@
import { defineAsyncComponent } from 'vue'
import { format } from 'date-fns'
import { id } from 'date-fns/locale'
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import type { ActionReportData } from '~/components/app/action-report/sample'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [{ width: 120 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 50 }],
headers: [
[
{ label: 'TANGGAL' },
{ label: 'INFORMASI UMUM' },
{ label: 'EDUKASI KHUSUS' },
{ label: 'RENCANA EDUKASI' },
{ label: 'PELAKSANAAN' },
{ label: 'AKSI' },
],
],
keys: ['reportAt', 'dpjp', 'operator', 'operationAt', 'operationType', 'action'],
delKeyNames: [
{ key: 'id', label: 'ID' },
{ key: 'dokter', label: 'Dokter' },
{ key: 'reportAt', label: 'Tanggal Laporan' },
],
parses: {
reportAt: (rec: unknown): unknown => {
const attr = (rec as ActionReportData).reportAt
const result = format(new Date(attr), 'd MMMM yyyy, HH:mm', { locale: id })
return result
},
operationAt: (rec: unknown): unknown => {
return '1 Rencana Edukasi'
},
system: (rec: unknown): unknown => {
return 'Cito'
},
operator: (rec: unknown): unknown => {
return '2 Edukasi dipilih'
},
billing: (rec: unknown): unknown => {
return 'General'
},
operationType: (rec: unknown): unknown => {
return '-'
},
dpjp: (rec: unknown): unknown => {
return '3 Informasi Dipilih'
},
parent: (rec: unknown): unknown => {
const recX = rec as any
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
@@ -0,0 +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'
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,75 @@
import { defineAsyncComponent } from 'vue'
import { format } from 'date-fns'
import { id } from 'date-fns/locale'
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import type { ActionReportData } from '~/components/app/action-report/sample'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [{ width: 120 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 50 }],
headers: [
[
{ label: 'TANGGAL' },
{ label: 'INFORMASI UMUM' },
{ label: 'EDUKASI KHUSUS' },
{ label: 'RENCANA EDUKASI' },
{ label: 'PELAKSANAAN' },
{ label: 'AKSI' },
],
],
keys: ['reportAt', 'dpjp', 'operator', 'operationAt', 'operationType', 'action'],
delKeyNames: [
{ key: 'id', label: 'ID' },
{ key: 'dokter', label: 'Dokter' },
{ key: 'reportAt', label: 'Tanggal Laporan' },
],
parses: {
reportAt: (rec: unknown): unknown => {
const attr = (rec as ActionReportData).reportAt
const result = format(new Date(attr), 'd MMMM yyyy, HH:mm', { locale: id })
return result
},
operationAt: (rec: unknown): unknown => {
return '1 Rencana Edukasi'
},
system: (rec: unknown): unknown => {
return 'Cito'
},
operator: (rec: unknown): unknown => {
return '2 Edukasi dipilih'
},
billing: (rec: unknown): unknown => {
return 'General'
},
operationType: (rec: unknown): unknown => {
return '-'
},
dpjp: (rec: unknown): unknown => {
return '3 Informasi Dipilih'
},
parent: (rec: unknown): unknown => {
const recX = rec as any
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
@@ -0,0 +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'
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,54 @@
import { addWeeks, formatISO } from 'date-fns'
export type ActionReportData = {
id: number
reportAt: string
operationAt: string
noRm: string
noBill: string
nama: string
jk: string
alamat: string
klinik: string
dokter: string
caraBayar: string
rujukan: string
ketRujukan: string
asal: string
}
export const sampleRows: ActionReportData[] = [
{
id: 1,
reportAt: formatISO(addWeeks(new Date(), -1)),
operationAt: formatISO(addWeeks(new Date(), 1)),
noRm: 'RM23311224',
noBill: '-',
nama: 'Ahmad Baidowi',
jk: 'L',
alamat: 'Jl Jaksa Agung S. No. 9',
klinik: 'Penyakit dalam',
dokter: 'Dr. Andreas Sutaji',
caraBayar: 'JKN',
rujukan: 'Faskes BPJS',
ketRujukan: 'RUMAH SAKIT - RS Lawang Medika - Malang',
asal: 'Rawat Jalan Reguler',
},
{
id: 2,
reportAt: new Date().toISOString(),
operationAt: formatISO(addWeeks(new Date(), 2)),
noRm: 'RM23455667',
noBill: '-',
nama: 'Abraham Sulaiman',
jk: 'L',
alamat: 'Purwantoro, Blimbing',
klinik: 'Penyakit dalam',
dokter: 'Dr. Andreas Sutaji',
caraBayar: 'JKN',
rujukan: 'Faskes BPJS',
ketRujukan: 'RUMAH SAKIT - RS Lawang Medika - Malang',
asal: 'Rawat Jalan Reguler',
},
// tambahkan lebih banyak baris contoh jika perlu
]
@@ -33,7 +33,7 @@ const {
const units = ref<Array<Item>>([])
async function fetchData() {
units.value = await getUnitLabelList({}, true)
units.value = await getUnitLabelList({})
}
const selectedUnitId = inject<Ref<string | null>>("selectedUnitId")!
@@ -0,0 +1 @@
export { default as SelectDoctor } from './select-doctor.vue'
@@ -0,0 +1,72 @@
<script setup lang="ts">
import { cn } from '~/lib/utils'
// type
import type { Doctor } from '~/models/doctor'
import { type Person, parseName } from '~/models/person'
// componenets
import * as DE from '~/components/pub/my-ui/doc-entry'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
const props = defineProps<{
fieldName: string
label: string
placeholder: string
doctors?: Doctor[]
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
isDisabled?: boolean
colSpan?: number
}>()
const { class: containerClass, labelClass, colSpan = 1, doctors = [] } = props
const opts = computed(() => {
return doctors.map((doc) => ({
value: doc.id,
label: parseName(doc.employee.person as Person),
}))
})
</script>
<template>
<DE.Cell
:col-span="colSpan"
:class="cn('select-field-group', fieldGroupClass, containerClass)"
>
<DE.Label
:label-for="fieldName"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
:id="fieldName"
v-bind="componentField"
:items="opts"
:placeholder="placeholder"
:is-disabled="isDisabled"
search-placeholder="Cari dokter..."
empty-message="Dokter tidak ditemukan"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,699 @@
<script setup lang="ts">
// Components
import * as DE from '~/components/pub/my-ui/doc-entry'
import { Button } from '~/components/pub/ui/button'
import { Input } from '~/components/pub/ui/input'
import * as CB from '~/components/pub/my-ui/combobox'
import DatepickerSingle from '~/components/pub/my-ui/datepicker/datepicker-single.vue'
import FileUpload from '~/components/pub/my-ui/form/file-field.vue'
// Types
import { IntegrationEncounterSchema, type IntegrationEncounterFormData } from '~/schemas/integration-encounter.schema'
import type { PatientEntity } from '~/models/patient'
// Helpers
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { refDebounced } from '@vueuse/core'
import type { Doctor } from '~/models/doctor'
// References
import { paymentMethodCodes } from '~/const/key-val/common'
// App things
import { genEncounter, type Encounter } from '~/models/encounter'
// Props
const props = defineProps<{
mode?: string
isLoading?: boolean
isReadonly?: boolean
isSepValid?: boolean
isMemberValid?: boolean
isCheckingSep?: boolean
doctorItems?: CB.Item[]
selectedDoctor: Doctor
// subSpecialist?: any[]
// specialists?: TreeItem[]
payments?: any[]
participantGroups?: any[]
seps: any[]
patient?: PatientEntity | null | undefined
objects?: any
}>()
// Model
const model = defineModel<Encounter>()
model.value = genEncounter()
// Common preparation
const defaultCBItems = [{ label: 'Pilih', value: '' }]
const paymentMethodItems = ref<any>({})
// Emit preparation
const emit = defineEmits<{
(e: 'onSelectDoctor', code: string): void
(e: 'event', menu: string, value?: any): void
(e: 'fetch', value?: any): void
}>()
// Validation schema
const { handleSubmit, errors, defineField, meta } = useForm<IntegrationEncounterFormData>({
validationSchema: toTypedSchema(IntegrationEncounterSchema),
})
// Bind fields and extract attrs
const [doctorCode, doctorCodeAttrs] = defineField('doctor_code')
const [unitCode, unitCodeAttrs] = defineField('unit_code')
const [registerDate, registerDateAttrs] = defineField('registerDate')
const [paymentMethodCode, paymentMethodCodeAttrs] = defineField('paymentMethod_code')
const [patientCategory, patientCategoryAttrs] = defineField('patientCategory')
const [cardNumber, cardNumberAttrs] = defineField('cardNumber')
const [sepType, sepTypeAttrs] = defineField('sepType')
const [sepNumber, sepNumberAttrs] = defineField('sepNumber')
const [patientName, patientNameAttrs] = defineField('patientName')
const [nationalIdentity, nationalIdentityAttrs] = defineField('nationalIdentity')
const [medicalRecordNumber, medicalRecordNumberAttrs] = defineField('medicalRecordNumber')
const [sepFile, sepFileAttrs] = defineField('sepFile')
const [sippFile, sippFileAttrs] = defineField('sippFile')
const patientId = ref('')
const sepReference = ref('')
const sepControlDate = ref('')
const sepTrafficStatus = ref('')
const diagnosis = ref('')
const noteReference = ref('Hanya diperlukan jika pembayaran jenis JKN')
const noteFile = ref('Gunakan file [.pdf, .jpg, .png] dengan ukuran maksimal 1MB')
const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const mode = props.mode !== undefined ? props.mode : 'add'
// SEP validation state from props
const isSepValid = computed(() => props.isSepValid || false)
const isCheckingSep = computed(() => props.isCheckingSep || false)
const isInsurancePayment = computed(() => ['insurance', 'jkn'].includes(paymentMethodCode.value))
const isDateLoading = ref(false)
const debouncedSepNumber = refDebounced(sepNumber, 500)
const debouncedCardNumber = refDebounced(cardNumber, 500)
const sepFileReview = ref<any>(null)
const sippFileReview = ref<any>(null)
const unitFullName = ref('') // Unit, specialist, subspecialist
const formRef = ref<HTMLFormElement | null>(null) // Expose submit method for parent component
if (mode === 'add') {
// Set default sepDate to current date in YYYY-MM-DD format
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
registerDate.value = `${year}-${month}-${day}`
}
watch(
() => props.selectedDoctor,
(doctor) => {
unitFullName.value = doctor.subspecialist?.name ?? doctor.specialist?.name ?? doctor.unit?.name ?? 'tidak diketahui'
model.value!.unit_code = doctor.unit_code || ''
model.value!.specialist_code = doctor.specialist_code || ''
model.value!.subspecialist_code = doctor.subspecialist_code || ''
},
)
// Sync props to form fields
watch(
() => props.objects,
(objects) => {
if (objects && Object.keys(objects).length > 0) {
patientName.value = objects?.patientName || ''
nationalIdentity.value = objects?.nationalIdentity || ''
medicalRecordNumber.value = objects?.medicalRecordNumber || ''
doctorCode.value = objects?.doctorCode || ''
paymentMethodCode.value = objects?.paymentMethodCode || ''
patientCategory.value = objects?.patientCategory || ''
cardNumber.value = objects?.cardNumber || ''
sepType.value = objects?.sepType || ''
sepNumber.value = objects?.sepNumber || ''
sepFileReview.value = objects?.sepFileReview || ''
sippFileReview.value = objects?.sippFileReview || ''
isDateLoading.value = true
setTimeout(() => {
registerDate.value = objects?.registerDate || ''
isDateLoading.value = false
}, 100)
}
},
{ deep: true, immediate: true },
)
watch(
() => props.patient,
(patient) => {
if (patient && Object.keys(patient).length > 0) {
patientId.value = patient?.id ? String(patient.id) : ''
patientName.value = patient?.person?.name || ''
nationalIdentity.value = patient?.person?.residentIdentityNumber || ''
medicalRecordNumber.value = patient?.number || ''
}
},
{ deep: true, immediate: true },
)
watch(
() => props.isSepValid,
(value) => {
if (!value) return
const objects = props.objects
if (objects && Object.keys(objects).length > 0) {
sepReference.value = objects?.sepReference || ''
sepControlDate.value = objects?.sepControlDate || ''
sepTrafficStatus.value = objects?.sepTrafficStatus || ''
diagnosis.value = objects?.diagnosis || ''
}
},
)
watch(debouncedSepNumber, (newValue) => {
emit('event', 'sep-number-changed', newValue)
})
watch(debouncedCardNumber, (newValue) => {
emit('event', 'member-changed', newValue)
})
function onAddSep() {
const formValues = {
patientId: patientId.value || '',
doctorCode: doctorCode.value,
registerDate: registerDate.value,
cardNumber: cardNumber.value,
paymentMethodCode: paymentMethodCode.value,
sepFile: sepFile.value,
sippFile: sippFile.value,
sepType: sepType.value,
}
emit('event', 'add-sep', formValues)
}
function onSearchSep() {
emit('event', 'search-sep', { cardNumber: cardNumber.value })
}
// Submit handler
const onSubmit = handleSubmit((values) => {
let payload: any = values
if (props.mode === 'edit') {
payload = {
...payload,
sepFileReview: sepFileReview.value,
sippFileReview: sippFileReview.value,
}
}
emit('event', 'save', payload)
})
function openFile(path: string) {
window.open(path, '_blank')
}
function submitForm() {
// Trigger form submit using native form submit
// This will trigger validation and onSubmit handler
if (formRef.value) {
formRef.value.requestSubmit()
} else {
// Fallback: directly call onSubmit handler
// Create a mock event object
const mockEvent = {
preventDefault: () => {},
target: formRef.value || {},
} as SubmitEvent
// Call onSubmit directly
onSubmit(mockEvent)
}
}
defineExpose({
submitForm,
})
onMounted(() => {
const isPaymentMethodVclaim = true
paymentMethodItems.value = isPaymentMethodVclaim ? props.payments : CB.recStrToItem(paymentMethodCodes)
})
</script>
<template>
<div class="mx-auto w-full">
<form
ref="formRef"
@submit.prevent="onSubmit"
class="grid gap-6 p-4"
>
<!-- Data Pasien -->
<div class="flex flex-col gap-2">
<h3 class="text-lg font-semibold">Data Pasien</h3>
<div class="flex items-center gap-2">
<span class="text-sm">sudah pernah terdaftar sebagai pasien?</span>
<Button
variant="outline"
type="button"
class="h-[40px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50"
@click="emit('event', 'search')"
>
<Icon
name="i-lucide-search"
class="h-5 w-5"
/>
Cari Pasien
</Button>
<span class="text-sm">belum pernah terdaftar sebagai pasien?</span>
<Button
variant="outline"
type="button"
class="h-[40px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50"
@click="emit('event', 'add')"
>
<Icon
name="i-lucide-plus"
class="h-5 w-5"
/>
Tambah Pasien Baru
</Button>
</div>
</div>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<DE.Label height="compact">Nama Pasien</DE.Label>
<DE.Field :errMessage="errors.patientName">
<Input
id="patientName"
v-model="patientName"
v-bind="patientNameAttrs"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">NIK</DE.Label>
<DE.Field :errMessage="errors.nationalIdentity">
<Input
id="nationalIdentity"
v-model="nationalIdentity"
v-bind="nationalIdentityAttrs"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">No. RM</DE.Label>
<DE.Field :errMessage="errors.medicalRecordNumber">
<Input
id="medicalRecordNumber"
v-model="medicalRecordNumber"
v-bind="medicalRecordNumberAttrs"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
<hr />
<!-- Data Kunjungan -->
<h3 class="text-lg font-semibold">Data Kunjungan</h3>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<DE.Label height="compact">
Dokter
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.doctor_code">
<CB.Combobox
id="doctorCode"
v-model="doctorCode"
v-bind="doctorCodeAttrs"
:items="[...defaultCBItems, ...doctorItems]"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih Dokter"
search-placeholder="Cari Dokter"
empty-message="Dokter tidak ditemukan"
@update:model-value="(value: any) => emit('onSelectDoctor', value)"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">
Spesialis / Subspesialis
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.unit_code">
<Input
:value="unitFullName"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">
Tanggal Daftar
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.registerDate">
<DatepickerSingle
v-if="!isDateLoading"
id="registerDate"
v-model="registerDate"
v-bind="registerDateAttrs"
placeholder="Pilih tanggal"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<DE.Label height="compact">
Jenis Pembayaran
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.paymentMethod_code">
<CB.Combobox
id="paymentMethodCode"
v-model="paymentMethodCode"
v-bind="paymentMethodCodeAttrs"
:items="paymentMethodItems"
:disabled="isLoading || isReadonly"
placeholder="Pilih Jenis Pembayaran"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
<!-- BPJS Fields (conditional) -->
<template v-if="isInsurancePayment">
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<DE.Label height="compact">
Kelompok Peserta
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.patientCategory">
<Select
id="patientCategory"
v-model="patientCategory"
v-bind="patientCategoryAttrs"
:items="participantGroups || []"
:disabled="isLoading || isReadonly"
placeholder="Pilih Kelompok Peserta"
/>
</DE.Field>
<span class="text-sm text-gray-500">
{{ noteReference }}
</span>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">
No. Kartu BPJS
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.cardNumber">
<Input
id="cardNumber"
v-model="cardNumber"
v-bind="cardNumberAttrs"
:disabled="isLoading || isReadonly"
placeholder="Masukkan nomor kartu BPJS"
/>
</DE.Field>
<div
v-if="isMemberValid"
class="mt-1 flex items-center gap-2"
>
<Icon
name="i-lucide-badge-check"
class="h-4 w-4 bg-green-500 text-white"
/>
<span class="text-sm text-green-500">Aktif</span>
</div>
<div
v-if="!isMemberValid"
class="mt-1 flex items-center gap-2"
>
<Icon
name="i-lucide-x"
class="h-4 w-4 bg-red-500 text-white"
/>
<span class="text-sm text-red-500">Tidak aktif</span>
</div>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">
Jenis SEP
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.sepType">
<Select
id="sepType"
v-model="sepType"
v-bind="sepTypeAttrs"
:items="seps"
:disabled="isLoading || isReadonly"
placeholder="Pilih Jenis SEP"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<DE.Label height="compact">
No. SEP
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.sepNumber">
<div class="flex gap-2">
<Input
id="sepNumber"
v-model="sepNumber"
v-bind="sepNumberAttrs"
placeholder="Tambah SEP terlebih dahulu"
class="flex-1"
:disabled="isLoading || isReadonly || isSepValid"
/>
<Button
v-if="!isSepValid"
variant="outline"
type="button"
class="bg-primary"
size="sm"
:disabled="isCheckingSep || isLoading || isReadonly"
@click="onAddSep"
>
<Icon
v-if="isCheckingSep"
name="i-lucide-loader-2"
class="h-4 w-4 animate-spin"
/>
<Icon
v-else
name="i-lucide-plus"
class="h-4 w-4"
/>
</Button>
<Button
v-if="isMemberValid"
variant="outline"
type="button"
class="bg-primary"
size="sm"
@click="onSearchSep"
>
<Icon
name="i-lucide-search"
class="h-4 w-4"
/>
</Button>
</div>
</DE.Field>
<div
v-if="isSepValid"
class="mt-1 flex items-center gap-2"
>
<Icon
name="i-lucide-badge-check"
class="h-4 w-4 bg-green-500 text-white"
/>
<span class="text-sm text-green-500">Aktif</span>
</div>
<span class="text-sm text-gray-500">
{{ noteReference }}
</span>
</DE.Cell>
<DE.Cell>
<FileUpload
field-name="sepFile"
label="Dokumen SEP"
placeholder="Pilih file"
:accept="['pdf', 'jpg', 'png']"
:max-size-mb="1"
v-model="sepFile"
v-bind="sepFileAttrs"
@file-selected="() => {}"
/>
<span class="mt-1 text-sm text-gray-500">
{{ noteFile }}
</span>
<p v-if="sepFileReview">
<a
class="mt-1 text-sm capitalize text-blue-500"
href="#"
@click="openFile(sepFileReview.filePath)"
>
{{ sepFileReview?.fileName }}
</a>
</p>
</DE.Cell>
<DE.Cell>
<FileUpload
field-name="sippFile"
label="Dokumen SIPP"
placeholder="Pilih file"
:accept="['pdf', 'jpg', 'png']"
:max-size-mb="1"
v-model="sippFile"
v-bind="sippFileAttrs"
@file-selected="() => {}"
/>
<span class="mt-1 text-sm text-gray-500">
{{ noteFile }}
</span>
<p v-if="sippFileReview">
<a
class="mt-1 text-sm capitalize text-blue-500"
href="#"
@click="openFile(sippFileReview.filePath)"
>
{{ sippFileReview?.fileName }}
</a>
</p>
</DE.Cell>
</DE.Block>
</template>
<template v-if="isSepValid">
<hr />
<!-- Data SEP -->
<h3 class="text-lg font-semibold">Data SEP</h3>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell>
<Label height="compact">Dengan Rujukan / Surat Kontrol</Label>
<DE.Field>
<Input
id="sepReference"
v-model="sepReference"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<Label height="compact">No. Rujukan / Surat Kontrol</Label>
<DE.Field>
<Input
id="sepReference"
v-model="sepNumber"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<Label height="compact">
Tanggal Rujukan / Surat Kontrol
<span class="ml-1 text-red-500">*</span>
</Label>
<DE.Field>
<DatepickerSingle
id="sepControlDate"
v-model="sepControlDate"
:disabled="true"
placeholder="Pilih tanggal sep"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="3"
:cellFlex="false"
>
<DE.Cell :col-span="2">
<Label height="compact">Diagnosis</Label>
<DE.Field>
<Input
id="diagnosis"
v-model="diagnosis"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<Label height="compact">Status Kecelakaan</Label>
<DE.Field>
<Input
id="sepTrafficStatus"
v-model="sepTrafficStatus"
:disabled="true"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
</template>
</form>
</div>
</template>
+12 -13
View File
@@ -23,7 +23,6 @@ import { paymentMethodCodes } from '~/const/key-val/common'
// App things
import { genEncounter, type Encounter } from '~/models/encounter'
import { se } from 'date-fns/locale'
// Props
const props = defineProps<{
@@ -50,7 +49,6 @@ model.value = genEncounter()
// Common preparation
const defaultCBItems = [{ label: 'Pilih', value: '' }]
const paymentMethodItems = CB.recStrToItem(paymentMethodCodes)
// Emit preparation
const emit = defineEmits<{
@@ -85,12 +83,10 @@ const sepTrafficStatus = ref('')
const diagnosis = ref('')
const noteReference = ref('Hanya diperlukan jika pembayaran jenis JKN')
const noteFile = ref('Gunakan file [.pdf, .jpg, .png] dengan ukuran maksimal 1MB')
const mode = props.mode !== undefined ? props.mode : 'add'
const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const mode = props.mode !== undefined ? props.mode : 'add'
// SEP validation state from props
const isSepValid = computed(() => props.isSepValid || false)
const isSepValid = computed(() => props.isSepValid || false) // SEP validation state from props
const isCheckingSep = computed(() => props.isCheckingSep || false)
const isInsurancePayment = computed(() => ['insurance', 'jkn'].includes(paymentMethodCode.value))
const isDateLoading = ref(false)
@@ -100,6 +96,7 @@ const sepFileReview = ref<any>(null)
const sippFileReview = ref<any>(null)
const unitFullName = ref('') // Unit, specialist, subspecialist
const formRef = ref<HTMLFormElement | null>(null) // Expose submit method for parent component
const paymentMethodItems = CB.recStrToItem(paymentMethodCodes)
if (mode === 'add') {
// Set default sepDate to current date in YYYY-MM-DD format
@@ -129,13 +126,15 @@ watch(
nationalIdentity.value = objects?.nationalIdentity || ''
medicalRecordNumber.value = objects?.medicalRecordNumber || ''
doctorCode.value = objects?.doctorCode || ''
paymentMethodCode.value = objects?.paymentMethodCode || ''
patientCategory.value = objects?.patientCategory || ''
cardNumber.value = objects?.cardNumber || ''
sepType.value = objects?.sepType || ''
sepNumber.value = objects?.sepNumber || ''
sepFileReview.value = objects?.sepFileReview || ''
sippFileReview.value = objects?.sippFileReview || ''
if (objects.paymentType) {
paymentMethodCode.value = objects.paymentType || ''
}
isDateLoading.value = true
setTimeout(() => {
registerDate.value = objects?.registerDate || ''
@@ -348,7 +347,7 @@ defineExpose({
placeholder="Pilih Dokter"
search-placeholder="Cari Dokter"
empty-message="Dokter tidak ditemukan"
@update:model-value="(value) => emit('onSelectDoctor', value)"
@update:model-value="(value: any) => emit('onSelectDoctor', value)"
/>
</DE.Field>
</DE.Cell>
@@ -395,12 +394,12 @@ defineExpose({
<span class="text-red-500">*</span>
</DE.Label>
<DE.Field :errMessage="errors.paymentMethod_code">
<CB.Combobox
<Select
id="paymentMethodCode"
v-model="paymentMethodCode"
v-bind="paymentMethodCodeAttrs"
:items="paymentMethodItems"
:disabled="isLoading || isReadonly"
:items="payments || []"
:disabled="isLoading || isReadonly || mode === 'edit'"
placeholder="Pilih Jenis Pembayaran"
/>
</DE.Field>
@@ -576,7 +575,7 @@ defineExpose({
</span>
<p v-if="sepFileReview">
<a
class="mt-1 text-sm text-blue-500 capitalize"
class="mt-1 text-sm capitalize text-blue-500"
href="#"
@click="openFile(sepFileReview.filePath)"
>
@@ -601,7 +600,7 @@ defineExpose({
</span>
<p v-if="sippFileReview">
<a
class="mt-1 text-sm text-blue-500 capitalize"
class="mt-1 text-sm capitalize text-blue-500"
href="#"
@click="openFile(sippFileReview.filePath)"
>
+62 -78
View File
@@ -85,10 +85,8 @@ const isExcluded = (key: string) => props.excludeFields?.includes(key)
</Cell>
</Block>
<Separator class="mt-8" />
<div class="my-2 flex items-center justify-between">
<h1 class="font-semibold">Anggota Keluarga</h1>
<div class="text-sm 2xl:text-base font-semibold">Anggota Keluarga</div>
<Button
type="button"
@click="addRelative"
@@ -102,7 +100,7 @@ const isExcluded = (key: string) => props.excludeFields?.includes(key)
:key="idx"
class="my-2 rounded-md border border-slate-300 p-4"
>
<Block :colCount="2">
<Block :colCount="2" :cell-flex="false" class="!mb-2">
<Cell>
<Label dynamic>Nama Anggota Keluarga</Label>
<Field>
@@ -118,97 +116,83 @@ const isExcluded = (key: string) => props.excludeFields?.includes(key)
</Cell>
</Block>
<button
<Button
variant="destructive"
type="button"
class="mt-3 text-sm text-red-500"
@click="removeRelative(idx)"
>
Hapus
</button>
</Button>
</div>
<Separator class="mt-8" />
<!-- Responsible Section -->
<div class="my-2">
<h1 class="font-semibold">Penanggung Jawab</h1>
<div class="mt-8 mb-1.5">
<div class="text-sm 2xl:text-base font-semibold">Penanggung Jawab</div>
</div>
<Block :col-count="2" :cell-flex="false">
<Cell>
<Label dynamic>Nama Penanggung Jawab</Label>
<Field>
<Input
v-model="responsibleName"
v-bind="responsibleNameAttrs"
/>
</Field>
</Cell>
<div class="my-2 rounded-md border border-slate-300 p-4">
<Block :colCount="2">
<Cell>
<Label dynamic>Nama Penanggung Jawab</Label>
<Field>
<Input
v-model="responsibleName"
v-bind="responsibleNameAttrs"
/>
</Field>
</Cell>
<Cell>
<Label dynamic>No. Hp Penanggung Jawab</Label>
<Field>
<Input
v-model="responsiblePhone"
v-bind="responsiblePhoneAttrs"
/>
</Field>
</Cell>
</Block>
</div>
<Separator class="mt-8" />
<Cell>
<Label dynamic>No. Hp Penanggung Jawab</Label>
<Field>
<Input
v-model="responsiblePhone"
v-bind="responsiblePhoneAttrs"
/>
</Field>
</Cell>
</Block>
<!-- Informant -->
<div class="my-2">
<h1 class="font-semibold">Pemberi Informasi</h1>
<div class="mt-8 mb-1.5">
<div class="text-sm 2xl:text-base font-semibold">Pemberi Informasi</div>
</div>
<Block :col-count="2" :cell-flex="false">
<Cell>
<Label dynamic>Informant</Label>
<Field>
<Input
v-model="informant"
v-bind="informantAttrs"
/>
</Field>
</Cell>
</Block>
<div class="my-2 rounded-md border border-slate-300 p-4">
<Block>
<Cell>
<Label dynamic>Informant</Label>
<Field>
<Input
v-model="informant"
v-bind="informantAttrs"
/>
</Field>
</Cell>
</Block>
</div>
<Separator class="mt-8" />
<!-- Witnesses -->
<div class="my-2">
<h1 class="font-semibold">Saksi</h1>
<div class="mt-8 mb-1.5">
<div class="text-sm 2xl:text-base font-semibold">Saksi</div>
</div>
<Block :col-count="2" :cell-flex="false">
<Cell>
<Label dynamic>Saksi 1</Label>
<Field>
<Input
v-model="witness1"
v-bind="witness1Attrs"
/>
</Field>
</Cell>
<div class="my-2 rounded-md border border-slate-300 p-4">
<Block :colCount="2">
<Cell>
<Label dynamic>Saksi 1</Label>
<Field>
<Input
v-model="witness1"
v-bind="witness1Attrs"
/>
</Field>
</Cell>
<Cell>
<Label dynamic>Saksi 2</Label>
<Field>
<Input
v-model="witness2"
v-bind="witness2Attrs"
/>
</Field>
</Cell>
</Block>
</div>
<Cell>
<Label dynamic>Saksi 2</Label>
<Field>
<Input
v-model="witness2"
v-bind="witness2Attrs"
/>
</Field>
</Cell>
</Block>
</div>
</form>
</template>
@@ -6,7 +6,7 @@ type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
export const config: Config = {
cols: [{ width: 100 }, {}, {}, {}, { width: 50 }],
cols: [{ width: 100 }, {}, {}, {}, {}, {}, { width: 50 }],
headers: [
[
{ label: 'Tanggal' },
+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>
+19 -6
View File
@@ -13,20 +13,33 @@ const props = defineProps<{
Order {{ data?.createdAt?.substring(0, 10) }} - {{ data?.status_code }}
</div>
<div class="max-w-[1000px]">
<DE.Block mode="preview" :col-count=5 class="!mb-3">
<DE.Block :col-count=5 class="!mb-3" :cell-flex="false">
<DE.Cell :col-span="2">
<DE.Label class="font-semibold">DPJP</DE.Label>
<DE.Label>DPJP</DE.Label>
<DE.Field>
{{ data?.doctor?.employee?.person?.name || '.........' }}
<Input :value="data?.doctor?.employee?.person?.name || '.........'" readonly />
</DE.Field>
</DE.Cell>
<DE.Cell></DE.Cell>
<DE.Cell :col-span="2">
<DE.Label class="font-semibold">PPDS</DE.Label>
<DE.Label>PPDS</DE.Label>
<DE.Field>
...........
<Input :value="'.........'" readonly />
</DE.Field>
</DE.Cell>
<DE.Cell :col-span="2">
<DE.Label>Pemeriksaan Ke</DE.Label>
<DE.Field>
<Input readonly />
</DE.Field>
</DE.Cell>
<DE.Cell></DE.Cell>
<DE.Cell :col-span="2">
<DE.Label>Temperatur Aksiler</DE.Label>
<DE.Field>
<Input />
</DE.Field>
</DE.Cell>
</DE.Block>
</div>
</template>
</template>
@@ -0,0 +1,157 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
import * as Cbx from '~/components/pub/my-ui/checkboxes'
import { fi } from 'date-fns/locale';
const substances = [
{ value: 'biopsi', label: 'Biopsi'},
{ value: 'operation', label: 'Operasi' },
{ value: 'scraping', label: 'Kerokan' },
{ value: 'cytology', label: 'Sitologi' },
{ value: 'fnab', label: 'FNAB' },
]
const fications = [
{ value: 'pa-formaline-10p', label: 'pA. Formalin 10%' },
{ value: 'spt-alcohol-70p', label: 'Sputum Alkohol 70%' },
{ value: 'urn-alcohol-50p', label: 'Urine Alkohol 50%' },
{ value: 'vgn-smr-alcohol-95p', label: 'Vagibal Smear 95%' },
]
const prevAp = [
{ value: 'clinical', label: 'Klinik' },
{ value: 'ro', label: 'RO' },
{ value: 'clinical-pat', label: 'Pat. Klinik' },
{ value: 'operation', label: 'Operasi' },
{ value: 'necropsy', label: 'Nekropsi' },
]
</script>
<template>
<div class="header">Data Order</div>
<DE.Block :col-count="3" :cell-flex="false">
<DE.Cell>
<DE.Label>Tgl. Order</DE.Label>
<DE.Field>
<Input readonly />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>DPJP</DE.Label>
<DE.Field>
<Input readonly />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>PPDS</DE.Label>
<DE.Field>
<Input readonly />
</DE.Field>
</DE.Cell>
</DE.Block>
<div class="header">Bahan</div>
<DE.Block :cell-flex="false">
<DE.Cell>
<DE.Field>
<Cbx.Checkboxes :items="substances" :use-flex="true" />
</DE.Field>
</DE.Cell>
</DE.Block>
<div class="header">Fiksasi</div>
<DE.Block :cell-flex="false">
<DE.Cell>
<DE.Field>
<Cbx.Checkboxes :items="fications" :use-flex="true" />
</DE.Field>
</DE.Cell>
</DE.Block>
<div class="header">Data Klinis Pasien</div>
<DE.Block :col-count="6" :cell-flex="false">
<DE.Cell :col-span="3">
<DE.Label>Lokalisasi</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
<DE.Cell :col-span="3">
<DE.Label>Diagnosa Klinik</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Stadium T</DE.Label>
<DE.Field>
<Input />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Stadium M</DE.Label>
<DE.Field>
<Input />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Stadium N</DE.Label>
<DE.Field>
<Input />
</DE.Field>
</DE.Cell>
<div class=""></div>
<DE.Cell :col-span="3">
<DE.Label>Keterangan Klinik</DE.Label>
<DE.Field>
<Textarea rows="2" />
</DE.Field>
</DE.Cell>
</DE.Block>
<div class="header">Riwayat Pemeriksaan</div>
<DE.Block :col-count="2" :cell-flex="false">
<DE.Cell>
<DE.Label>Riwayat Dulu</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Riwayat Sekarang</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Pemeriksaan PA Sebelumnya</DE.Label>
<DE.Field>
<Cbx.Checkboxes :items="prevAp" :use-flex="true" />
</DE.Field>
</DE.Cell>
<div></div>
<DE.Cell>
<DE.Label>Keterangan PA Sebelumnya</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Pemeriksaan Penunjang</DE.Label>
<DE.Field>
<Textarea />
</DE.Field>
</DE.Cell>
</DE.Block>
</template>
<style>
.header {
@apply mb-1.5 text-sm 2xl:text-base font-semibold
}
</style>
@@ -41,8 +41,11 @@ defineExpose({
<template>
<form @submit.prevent>
<div>
<p class="text-md mb-2 mt-1 font-semibold">
{{ title }}
<p
v-if="props.title"
class="mb-2 text-sm font-semibold 2xl:mb-3 2xl:text-base"
>
{{ props.title || 'Kontak Pasien' }}
</p>
</div>
<div class="mb-5 space-y-4">
+147
View File
@@ -0,0 +1,147 @@
<script setup lang="ts">
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'
// Helpers
import type z from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
// import { ref, watch, inject } from 'vue'
const props = defineProps<{
modelValue: any
schema: z.ZodSchema<any>
excludeFields?: string[]
isReadonly?: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', val: any): void
(e: 'submit', val: any): void
}>()
// Setup form
const {
validate: _validate,
defineField,
handleSubmit,
errors,
values,
} = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: props.modelValue,
})
watch(values, (val) => emit('update:modelValue', val), { deep: true })
// Define form fields
// const [relatives, relativesAttrs] = defineField('relatives')
const [date, dateAttrs] = defineField('date')
const [doctor, doctorAttrs] = defineField('doctor')
const [diagnosis, diagnosisAttrs] = defineField('diagnosis')
const [essay, essayAttrs] = defineField('essay')
const [plan, planAttrs] = defineField('plan')
const [note, noteAttrs] = defineField('note')
const tanggal = ref('')
const tanggalAttrs = ref({
type: 'date',
required: true,
disabled: props.isReadonly,
})
// Relatives list handling
const addRelative = () => {
relatives.value = [...(relatives.value || []), { name: '', phone: '' }]
}
const removeRelative = (index: number) => {
relatives.value = relatives.value.filter((_: any, i: number) => i !== index)
}
const validate = async () => {
const result = await _validate()
return {
valid: true,
data: result.values,
errors: result.errors,
}
}
defineExpose({ validate })
const icdPreview = inject('icdPreview')
const isExcluded = (key: string) => props.excludeFields?.includes(key)
</script>
<template>
<form id="entry-form">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<Block :colCount="2">
<Cell>
<Label dynamic>Tanggal</Label>
<Field>
<Input
v-model="date"
v-bind="dateAttrs"
type="date"
/>
</Field>
</Cell>
<Cell>
<Label dynamic>Dokter</Label>
<Field>
<Input
v-model="doctor"
v-bind="doctorAttrs"
/>
</Field>
</Cell>
</Block>
<Block :colCount="2">
<Cell>
<Label dynamic>Diagnosis Penting</Label>
<Field>
<Textarea
v-model="diagnosis"
v-bind="diagnosisAttrs"
/>
</Field>
</Cell>
<Cell>
<Label dynamic>Uraian Klinik Penting</Label>
<Field>
<Textarea
v-model="essay"
v-bind="essayAttrs"
/>
</Field>
</Cell>
</Block>
<Block :colCount="2">
<Cell>
<Label dynamic>Rencana Penting</Label>
<Field>
<Textarea
v-model="plan"
v-bind="planAttrs"
/>
</Field>
</Cell>
<Cell>
<Label dynamic>Catatan</Label>
<Field>
<Textarea
v-model="note"
v-bind="noteAttrs"
/>
</Field>
</Cell>
</Block>
<Separator class="mt-8" />
</div>
</form>
</template>
@@ -0,0 +1,82 @@
import type { Config, RecComponent, RecStrFuncComponent, RecStrFuncUnknown } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
import type { GeneralConsent } from '~/models/general-consent'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
export const config: Config = {
cols: [{ width: 100 }, {}, {}, {}, { width: 50 }],
headers: [
[
{ label: 'Tanggal' },
{ label: 'Anggota Keluarga' },
{ label: 'Penanggung Jawab' },
{ label: 'Pemberi Informasi' },
{ label: 'Saksi 1' },
{ label: 'Saksi 2' },
{ label: '' },
],
],
keys: ['date', 'relatives', 'responsible', 'informant', 'witness1', 'witness2', 'action'],
delKeyNames: [
{ key: 'data', label: 'Tanggal' },
{ key: 'dstDoctor.name', label: 'Dokter' },
],
parses: {
date(rec) {
const recX = rec as GeneralConsent
return recX?.createdAt?.substring(0, 10) || '-'
},
relatives(rec) {
const recX = rec as GeneralConsent
const parsed = JSON.parse(recX?.value || '{}')
return parsed?.relatives?.join(', ') || '-'
},
responsible(rec) {
const recX = rec as GeneralConsent
const parsed = JSON.parse(recX?.value || '{}')
return parsed?.responsible || '-'
},
informant(rec) {
const recX = rec as GeneralConsent
const parsed = JSON.parse(recX?.value || '{}')
return parsed?.informant || '-'
},
witness1(rec) {
const recX = rec as GeneralConsent
const parsed = JSON.parse(recX?.value || '{}')
return parsed?.witness1 || '-'
},
witness2(rec) {
const recX = rec as GeneralConsent
const parsed = JSON.parse(recX?.value || '{}')
return parsed?.witness2 || '-'
},
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
props: {
size: 'sm',
},
}
return res
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
props: {
size: 'sm',
},
}
return res
},
} as RecStrFuncComponent,
htmls: {} as RecStrFuncUnknown,
}
+34
View File
@@ -0,0 +1,34 @@
<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
}
const props = 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"
/>
<!-- FIXME: pindahkan ke content/division/list.vue -->
<PaginationView
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
</template>
@@ -42,7 +42,7 @@ const itemsCount = computed(() => items.length || 0)
{{ item?.createdAt.toLocaleDateString('id-ID') }}
</time>
<h1 :class="cn('text-gray-500 dark:text-gray-400', '')">Ditambahkan Oleh : {{ item.updatedBy }}</h1>
<NuxtLink class="mt-1 text-orange-500" :to="`surgery-report/${item.id}`">
<NuxtLink class="mt-1 text-orange-500" :to="`process?menu=surgery-report&mode=list&record-id=${item.id}`">
Lihat Detail
</NuxtLink>
</div>
@@ -45,6 +45,7 @@ const {
handleSearch,
fetchData: getItemList,
} = usePaginatedList({
syncToUrl: false,
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
@@ -102,19 +103,23 @@ onMounted(async () => {
</script>
<template>
<Dialog v-model:open="isModalOpen" title="" size="xl">
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class=""
/>
<AppProcedureSrcList
:table-config="config"
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog
v-model:open="isModalOpen"
title=""
size="xl"
>
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class=""
/>
<AppProcedureSrcList
:table-config="config"
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</Dialog>
</template>
@@ -1,62 +1,143 @@
<script setup lang="ts">
import ProcedureListDialog from './procedure-list.vue'
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import { Form } from '~/components/pub/ui/form'
import { FieldArray } from 'vee-validate'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import * as DE from '~/components/pub/my-ui/doc-entry'
import TextAreaInput from '~/components/pub/my-ui/form/text-area-input.vue'
// Types
import { type ProcedureSrc } from '~/models/procedure-src'
// Helper
import { cn } from '~/lib/utils'
// Components
import { FieldArray } from 'vee-validate'
import { ButtonAction } from '~/components/pub/my-ui/form'
import TableHeader from '~/components/pub/ui/table/TableHeader.vue'
import { is } from 'date-fns/locale'
import ProcedureListDialog from './procedure-list.vue'
interface Props {
fieldName: string
title: string
subTitle?: string
// State UI (Loading / Disabled)
isReadonly?: boolean
// Data Architecture Switch
// 'form' = Pakai Vee-Validate (Parent wajib useForm)
// 'preview' = Pakai Props sampleItems (Parent bebas)
mode?: 'form' | 'preview'
// Data Source untuk mode 'preview' (atau initial data)
sampleItems?: ProcedureSrc[]
}
const props = defineProps<Props>()
// Set default mode ke 'form' agar backward compatible
const props = withDefaults(defineProps<Props>(), {
mode: 'form',
isReadonly: false,
sampleItems: () => [],
})
const isProcedurePickerDialogOpen = ref<boolean>(false)
provide(`isProcedurePickerDialogOpen`, isProcedurePickerDialogOpen)
</script>
<template>
<div class="">
<div class="mb-2 flex items-center justify-between">
<h1 class="mb-2 font-medium">{{ title }}</h1>
<Button @click="isProcedurePickerDialogOpen = true" size="xs" variant="outline"
class="text-orange-400 border-orange-400 bg-transparent">
<Icon name="i-lucide-search" class="h-4 w-4 align-middle transition-colors" />
Pilih Diagnosis
</Button>
<div class="mb-2 flex items-center justify-between">
<p class="mb-2 font-medium">{{ title }}</p>
<ButtonAction
v-if="mode === 'form' && !isReadonly"
preset="add"
title="Tambah Item"
icon="i-lucide-search"
:label="subTitle || 'Pilih Diagnosis'"
:full-width-mobile="true"
@click="isProcedurePickerDialogOpen = true"
/>
</div>
<FieldArray
v-if="mode === 'form'"
v-slot="{ fields, push, remove }"
:name="props.fieldName"
>
<ProcedureListDialog :process-fn="push" />
<div class="overflow-hidden rounded-lg border border-gray-200">
<Table>
<TableHeader class="bg-gray-100">
<TableRow>
<TableHead class="w-1/2">Prosedur</TableHead>
<TableHead class="w-1/2">ICD-X</TableHead>
<TableHead class="w-[50px]">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="fields.length === 0">
<TableCell
colspan="3"
class="py-4 text-center text-muted-foreground"
>
Belum ada data dipilih.
</TableCell>
</TableRow>
<TableRow
v-for="(field, idx) in fields"
:key="field.key"
>
<TableCell :class="cn(isReadonly && 'opacity-50')">
{{ (field.value as ProcedureSrc)?.name }}
</TableCell>
<TableCell :class="cn(isReadonly && 'opacity-50')">
{{ (field.value as ProcedureSrc)?.code }}
</TableCell>
<TableCell>
<ButtonAction
v-if="!isReadonly"
preset="delete"
icon-only
:title="`Hapus ${(field.value as ProcedureSrc)?.name}`"
@click="remove(idx)"
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</FieldArray>
<FieldArray v-slot="{ fields, push, remove }" :name="props.fieldName">
<ProcedureListDialog :process-fn="push" />
<div
v-else
class="overflow-hidden rounded-lg border border-gray-200"
>
<Table>
<TableHeader class="bg-gray-100">
<TableRow>
<TableHead class="w-1/2">Prosedur</TableHead>
<TableHead class="w-1/2">ICD-X</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="sampleItems.length === 0">
<TableCell
colspan="2"
class="py-4 text-center text-muted-foreground"
>
Tidak ada data.
</TableCell>
</TableRow>
<div class="border border-gray-200 rounded-lg overflow-hidden">
<Table>
<TableHeader class="bg-gray-100">
<TableRow>
<TableHead class="w-1/2">Prosedur</TableHead>
<TableHead class="w-1/2">ICD-X</TableHead>
<TableHead class="w-1/2">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="(field, idx) in fields" :key="idx">
<TableCell class="">{{ field.value?.name }}</TableCell>
<TableCell class="">{{ field.value?.code }}</TableCell>
<TableCell class="">
<Button type="button" variant="destructive" size="sm" @click="remove(idx)">
<Icon name="i-lucide-trash-2" class="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</FieldArray>
<TableRow
v-for="item in sampleItems"
:key="item.code || item.id"
>
<TableCell class="text-muted-foreground">
{{ item.name }}
</TableCell>
<TableCell class="text-muted-foreground">
{{ item.code }}
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</template>
@@ -0,0 +1,34 @@
<script setup lang="ts">
import List from './list.vue'
import Form from './form.vue'
import View from './view.vue'
// Models
import type { Encounter } from '~/models/encounter'
// Props
interface Props {
encounter: Encounter
}
const props = defineProps<Props>()
const { mode, goToEntry, goToView } = useQueryCRUDMode('mode')
</script>
<template>
<div>
<List
v-if="mode === 'list'"
:encounter="props.encounter"
@add="goToEntry"
@edit="goToEntry({ fromView: false })"
@view="goToView"
/>
<View
v-else-if="mode === 'view'"
:encounter="props.encounter"
/>
<Form v-else />
</div>
</template>
@@ -0,0 +1,82 @@
<script setup lang="ts">
import mockData from './sample'
// type
import { genDoctor, type Doctor } from '~/models/doctor'
import type { ActionReportFormData } from '~/schemas/action-report.schema'
// components
import { toast } from '~/components/pub/ui/toast'
import AppActionReportEntry from '~/components/app/action-report/entry-form.vue'
import ArrangementProcedurePicker from '~/components/app/therapy-protocol/picker-dialog/arrangement-procedure/procedure-picker.vue'
// states
const route = useRoute()
const { mode, goBack } = useQueryCRUDMode('mode')
const { recordId } = useQueryCRUDRecordId('record-id')
const reportData = ref<ActionReportFormData>({} as unknown as ActionReportFormData)
const doctors = ref<Doctor[]>([])
const isLoading = ref<boolean>(false)
// TODO: dummy data
;(() => {
doctors.value = [genDoctor()]
})()
const entryMode = ref<'add' | 'edit' | 'view'>('add')
const isDataReady = ref(false)
onMounted(async () => {
if (mode.value === 'entry' && recordId.value) {
entryMode.value = 'edit'
await loadEntryForEdit(+recordId.value)
} else {
// Untuk mode 'add', langsung set ready
isDataReady.value = true
}
})
// TODO: map data
async function loadEntryForEdit(id: number | string) {
isLoading.value = true
const result = mockData
reportData.value = result as ActionReportFormData
isLoading.value = false
isDataReady.value = true
}
</script>
<template>
<AppActionReportEntry
v-if="isDataReady"
:isLoading="isLoading"
:mode="entryMode"
@submit="(val) => console.log(val)"
@back="goBack"
@error="
(err: Error) => {
toast({
title: 'Terjadi Kesalahan',
description: err.message,
variant: 'destructive',
})
}
"
:doctors="doctors"
:initialValues="reportData"
>
<template #procedures>
<ArrangementProcedurePicker
field-name="procedures"
title="Tindakan Operatif/Non-Operatif Lain"
sub-title="Pilih Prosedur"
/>
</template>
</AppActionReportEntry>
<div
v-else
class="flex items-center justify-center p-8"
>
<p class="text-muted-foreground">Memuat data...</p>
</div>
</template>
@@ -0,0 +1,276 @@
<script setup lang="ts">
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import AppActionReportList from '~/components/app/action-report/list.vue'
import AppActionReportListHistory from '~/components/app/action-report/list-history.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ButtonAction } from '~/components/pub/my-ui/form'
// config
import { config } from '~/components/app/action-report/list.cfg'
// types
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import type { Encounter } from '~/models/encounter'
// Samples
import { sampleRows, type ActionReportData } from '~/components/app/action-report/sample'
import sampleReport from './sample'
// helpers
import { toast } from '~/components/pub/ui/toast'
// Props
interface Props {
encounter: Encounter
}
const props = defineProps<Props>()
const emits = defineEmits<{
(e: 'add'): void
(e: 'edit', id: number | string): void
(e: 'view', id: number | string): void
}>()
// states
const router = useRouter()
const route = useRoute()
const { goToEntry, backToList } = useQueryCRUDMode('mode')
const title = ref('')
const search = ref('')
const dateFrom = ref('')
const dateTo = ref('')
const isDialogOpen = ref<boolean>(false)
const isLoading = ref<boolean>(false)
// #region mock
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/consultation.handler'
// #endregion
// filter + pencarian sederhana (client-side)
const filtered = computed(() => {
const q = search.value.trim().toLowerCase()
return sampleRows.filter((r: ActionReportData) => {
if (q) {
return r.nama.toLowerCase().includes(q) || r.noRm.toLowerCase().includes(q) || r.dokter.toLowerCase().includes(q)
}
return true
})
})
const goEdit = (id: number | string) => {
router.replace({
path: route.path,
query: {
...route.query,
mode: 'entry',
'record-id': id,
},
})
}
const goView = (id: number | string) => {
router.replace({
path: route.path,
query: {
...route.query,
mode: 'view',
'record-id': id,
},
})
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
async function onGetDetail(id: number | string) {
isLoading.value = true
const res = sampleReport
recItem.value = res
console.log(res)
isLoading.value = false
}
// #region watcher
watch([recId, recAction], (newVal) => {
const [id, action] = newVal
// Guard: jangan proses jika id = 0 atau action kosong
if (!id || !action) return
switch (action) {
case ActionEvents.showDetail:
// onGetDetail(recId.value)
goView(id)
title.value = 'Detail Konsultasi'
break
case ActionEvents.showEdit:
goEdit(id)
title.value = 'Edit Konsultasi'
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
// Reset KEDUANYA menggunakan nextTick agar tidak trigger watcher lagi
nextTick(() => {
recId.value = 0
recAction.value = ''
})
})
// #endregion
</script>
<template>
<div class="mx-auto max-w-full">
<div class="border-b p-6">
<h1 class="text-2xl font-semibold">Laporan Tindakan</h1>
<p class="mt-1 text-sm text-gray-500">Infomasi laporan tindakan pasien</p>
</div>
<div class="flex flex-wrap items-center gap-3 border-b p-4">
<div class="flex items-center gap-2">
<input
v-model="search"
placeholder="Cari Nama / No.RM"
class="w-64 rounded border px-3 py-2"
/>
</div>
<div class="flex items-center gap-2">
<input
v-model="dateFrom"
type="date"
class="rounded border px-3 py-2"
/>
<span class="text-sm text-gray-500">-</span>
<input
v-model="dateTo"
type="date"
class="rounded border px-3 py-2"
/>
<ButtonAction
preset="custom"
title="Filter List Laporan Tindakan"
label="Filter"
icon="i-lucide-filter"
@click="
() => {
isDialogOpen = true
}
"
/>
</div>
<div class="ml-auto flex items-center gap-2">
<ButtonAction
preset="custom"
title="Riwayat Laporan Tindakan"
icon="i-lucide-history"
label="Riwayat Laporan Tindakan"
@click="
() => {
isDialogOpen = true
}
"
/>
<ButtonAction
preset="add"
title="Tambah Data Laporan Tindakan"
icon="i-lucide-plus"
label="Tambah Data"
@click="
() => {
goToEntry()
}
"
/>
</div>
</div>
<div class="overflow-x-auto p-4">
<AppActionReportList
:data="filtered"
:pagination-meta="{
recordCount: 2,
page: 1,
pageSize: 10,
totalPage: 1,
hasPrev: false,
hasNext: false,
}"
/>
</div>
</div>
<Dialog
v-model:open="isDialogOpen"
title="Arsip Riwayat Laporan Tindakan"
size="2xl"
prevent-outside
@update:open="
(value: any) => {
isDialogOpen = value
}
"
>
<AppActionReportListHistory
:data="filtered"
:pagination-meta="{
recordCount: 2,
page: 1,
pageSize: 10,
totalPage: 1,
hasPrev: false,
hasNext: false,
}"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="
() =>
handleActionRemove(
recItem.id,
() => {
router.go(0)
},
toast,
)
"
@cancel=""
>
<template #default="{ record }">
{{ console.log(JSON.stringify(record)) }}
<div class="space-y-1 text-sm">
<p
v-for="field in config.delKeyNames"
:key="field.key"
:v-if="record?.[field.key]"
>
<span class="font-semibold">{{ field.label }}:</span>
{{ record[field.key] }}
</p>
</div>
</template>
</RecordConfirmation>
</template>
@@ -0,0 +1,68 @@
export default {
operatorTeam: {
dpjpId: -1,
operatorName: 'Julian Alvarez',
assistantOperatorName: 'Arda Guller',
instrumentNurseName: 'Kenan Yildiz',
surgeryDate: '2025-11-13T14:29:00',
actionDiagnosis: 'Sprei gratisnya mana',
},
procedures: [
{
id: -1,
name: 'Ndase mumet',
code: 'CX1',
},
],
operationExecution: {
surgeryType: 'khusus',
billingCode: 'local',
operationSystem: 'cito',
surgeryCleanType: 'kotor',
surgeryNumber: 'retry',
birthPlaceNote: 'out3',
personWeight: 100,
operationDescription: 'asdsadsa1',
birthRemark: 'lahir_hidup',
operationStartAt: '2025-11-13T14:29:00',
operationEndAt: '2025-11-13T17:29:00',
anesthesiaStartAt: '2025-11-13T11:29:00',
anesthesiaEndAt: '2025-11-13T18:29:00',
},
bloodInput: {
type: 'tc',
amount: {
prc: null,
wb: null,
ffp: null,
tc: 3243324,
},
},
implant: {
brand: 'Samsung',
name: 'S.Komedi',
companionName: 'When ya',
},
specimen: {
destination: 'pa',
},
tissueNotes: [
{
note: 'Anjai',
},
{
note: 'Ciee Kaget',
},
{
note: 'Baper',
},
{
note: 'Saltink weeh',
},
{
note: 'Kaburrr',
},
],
}
@@ -0,0 +1,67 @@
<script setup lang="ts">
import mockData from './sample'
// types
import { type ActionReportFormData } from '~/schemas/action-report.schema'
import { type Encounter } from '~/models/encounter'
// Components
import AppActionReportPreview from '~/components/app/action-report/preview.vue'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
// #region Props & Emits
const router = useRouter()
const { backToList, goToEntry } = useQueryCRUDMode('mode')
const { recordId } = useQueryCRUDRecordId('record-id')
function onEditFromView() {
goToEntry({ fromView: true })
}
const props = defineProps<{
encounter: Encounter
}>()
// #endregion
// #region State & Computed
const reportData = ref<ActionReportFormData | null>(null)
const headerPrep: HeaderPrep = {
title: 'Detail Laporan Tindakan',
icon: 'i-lucide-stethoscope',
}
// #endregion
// #region Lifecycle Hooks
onMounted(async () => {
reportData.value = mockData as unknown as ActionReportFormData
})
// #endregion
// #region Functions
// #endregion region
// #region Utilities & event handlers
function onEdit() {
router.push({
name: 'action-report-id-edit',
params: { id: 100 },
})
}
function onBack() {}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<AppActionReportPreview
v-if="reportData"
:data="reportData"
@back="backToList"
@edit="onEditFromView"
/>
</template>
@@ -0,0 +1,77 @@
<script setup lang="ts">
import Nav from '~/components/pub/my-ui/nav-footer/ba-de-su.vue'
import NavOk from '~/components/pub/my-ui/nav-footer/ok.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
import { type HeaderPrep } from '~/components/pub/my-ui/data/types'
// mcu src category
import ScrCategorySwitcher from '~/components/app/mcu-src-category/switcher.vue'
import { getList as getMcuCategoryList } from '~/services/mcu-src-category.service'
// mcu src
import { type McuSrc } from '~/models/mcu-src'
import { getList as getMcuSrcList } from '~/services/mcu-src.service'
import McuSrcPicker from '~/components/app/mcu-src/picker-accordion.vue'
// mcu order
import { getDetail } from '~/services/mcu-order.service'
import Detail from '~/components/app/mcu-order/detail.vue'
// mcu order item, manually not using composable
import {
getList as getMcuOrderItemList,
create as createMcuOrderItem,
remove as removeMcuOrderItem,
} from '~/services/mcu-order-item.service'
import { type McuOrderItem } from '~/models/mcu-order-item'
import Entry from '~/components/app/mcu-order/entry-for-ap.vue'
// props
const props = defineProps<{
encounter_id: number
}>()
// declaration & flows
// MCU Order
const { getQueryParam } = useQueryParam()
const id = getQueryParam('id')
const dataRes = await getDetail(
typeof id === 'string' ? parseInt(id) : 0,
{ includes: 'encounter,doctor,doctor-employee,doctor-employee-person' }
)
const data = dataRes.body?.data
// MCU Sources
const { backToList } = useQueryCRUDMode()
const headerPrep: HeaderPrep = {
title: 'Entry Order LAB PA',
icon: 'i-lucide-box',
}
function navClick(type: 'back' | 'delete' | 'draft' | 'submit') {
if (type === 'back') {
backToList()
}
}
</script>
<template>
<Header
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5"
/>
<Entry />
<Separator class="my-5" />
<div class="w-full flex justify-center">
<Nav @click="navClick" />
</div>
</template>
@@ -0,0 +1,159 @@
<script setup lang="ts">
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
handleActionSave,
handleActionRemove,
} from '~/handlers/mcu-order.handler'
// Apps
import { getList, getDetail } from '~/services/mcu-order.service'
import List from '~/components/app/mcu-order/list.vue'
import type { McuOrder } from '~/models/mcu-order'
const route = useRoute()
const { setQueryParams } = useQueryParam()
const { crudQueryParams } = useQueryCRUD()
const title = ref('')
const plainEid = route.params.id
const encounter_id = (plainEid && typeof plainEid == 'string') ? parseInt(plainEid) : 0 // here the
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getMyList,
} = usePaginatedList<McuOrder>({
fetchFn: async ({ page, search }) => {
const result = await getList({
search,
page,
'scope-code': "ap-lab",
'encounter-id': encounter_id,
includes: 'doctor,doctor-employee,doctor-employee-person',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'mcu-order'
})
const headerPrep: HeaderPrep = {
title: 'Order Lab PA',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (value: string) => {
searchInput.value = value
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
crudQueryParams.value = { mode: 'entry', recordId: undefined }
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getMyDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
// Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getMyDetail(recId.value)
title.value = 'Detail Order Lab PK'
isReadonly.value = true
break
case ActionEvents.showEdit:
getMyDetail(recId.value)
title.value = 'Edit Order Lab PK'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
watch([isFormEntryDialogOpen], async () => {
})
onMounted(async () => {
})
function cancel(data: McuOrder) {
recId.value = data.id
recItem.value = data
isRecordConfirmationOpen.value = true
}
function edit(data: McuOrder) {
setQueryParams({
'mode': 'entry',
'id': data.id.toString()
})
recItem.value = data
}
function submit(data: McuOrder) {
}
</script>
<template>
<Header :prep="{ ...headerPrep }" />
<List
v-if="!isLoading.dataListLoading"
:data="data"
:pagination-meta="paginationMeta"
@cancel="cancel"
@edit="edit"
@submit="submit"
/>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getMyList, toast)"
@cancel=""
/>
</template>
@@ -0,0 +1,16 @@
<script setup lang="ts">
//
import List from './list.vue'
import Entry from './entry.vue'
const props = defineProps<{
encounter_id: number
}>()
const { mode } = useQueryCRUDMode()
</script>
<template>
<List v-if="mode === 'list'" :encounter_id="encounter_id" />
<Entry v-else :encounter_id="encounter_id" />
</template>
@@ -0,0 +1,34 @@
<script setup lang="ts">
import List from './list.vue'
import Form from './form.vue'
import View from './view.vue'
// Models
import type { Encounter } from '~/models/encounter'
// Props
interface Props {
encounter: Encounter
}
const props = defineProps<Props>()
const { mode, goToEntry, goToView } = useQueryCRUDMode('mode')
</script>
<template>
<div>
<List
v-if="mode === 'list'"
:encounter="props.encounter"
@add="goToEntry"
@edit="goToEntry({ fromView: false })"
@view="goToView"
/>
<View
v-else-if="mode === 'view'"
:encounter="props.encounter"
/>
<Form v-else />
</div>
</template>
@@ -0,0 +1,74 @@
<script setup lang="ts">
import mockData from './sample'
// type
import { genDoctor, type Doctor } from '~/models/doctor'
import type { ActionReportFormData } from '~/schemas/action-report.schema'
// components
import { toast } from '~/components/pub/ui/toast'
import AppAssessmentEducationEntry from '~/components/app/assessment-education/entry.vue'
// states
const route = useRoute()
const { mode, goBack } = useQueryCRUDMode('mode')
const { recordId } = useQueryCRUDRecordId('record-id')
const reportData = ref<ActionReportFormData>({} as unknown as ActionReportFormData)
const doctors = ref<Doctor[]>([])
const isLoading = ref<boolean>(false)
// TODO: dummy data
;(() => {
doctors.value = [genDoctor()]
})()
const entryMode = ref<'add' | 'edit' | 'view'>('add')
const isDataReady = ref(false)
onMounted(async () => {
if (mode.value === 'entry' && recordId.value) {
entryMode.value = 'edit'
await loadEntryForEdit(+recordId.value)
} else {
// Untuk mode 'add', langsung set ready
isDataReady.value = true
}
})
// TODO: map data
async function loadEntryForEdit(id: number | string) {
isLoading.value = true
const result = mockData
reportData.value = result as ActionReportFormData
isLoading.value = false
isDataReady.value = true
}
</script>
<template>
<AppAssessmentEducationEntry
v-if="isDataReady"
:isLoading="isLoading"
:mode="entryMode"
@submit="(val: {}) => console.log(val)"
@back="goBack"
@error="
(err: Error) => {
toast({
title: 'Terjadi Kesalahan',
description: err.message,
variant: 'destructive',
})
}
"
:doctors="doctors"
:initialValues="reportData"
/>
<div
v-else
class="flex items-center justify-center p-8"
>
<p class="text-muted-foreground">Memuat data...</p>
</div>
</template>
@@ -0,0 +1,276 @@
<script setup lang="ts">
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import AppAssessmentEducationList from '~/components/app/assessment-education/list.vue'
import AppAssessmentEducationListHistory from '~/components/app/assessment-education/list-history.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ButtonAction } from '~/components/pub/my-ui/form'
// config
import { config } from '~/components/app/assessment-education/list.cfg'
// types
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import type { Encounter } from '~/models/encounter'
// Samples
import { sampleRows, type AssessmentEducationData } from '~/components/app/assessment-education/sample'
import sampleReport from './sample'
// helpers
import { toast } from '~/components/pub/ui/toast'
// Props
interface Props {
encounter: Encounter
}
const props = defineProps<Props>()
const emits = defineEmits<{
(e: 'add'): void
(e: 'edit', id: number | string): void
(e: 'view', id: number | string): void
}>()
// states
const router = useRouter()
const route = useRoute()
const { goToEntry, backToList } = useQueryCRUDMode('mode')
const title = ref('')
const search = ref('')
const dateFrom = ref('')
const dateTo = ref('')
const isDialogOpen = ref<boolean>(false)
const isLoading = ref<boolean>(false)
// #region mock
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/consultation.handler'
// #endregion
// filter + pencarian sederhana (client-side)
const filtered = computed(() => {
const q = search.value.trim().toLowerCase()
return sampleRows.filter((r: ActionReportData) => {
if (q) {
return r.nama.toLowerCase().includes(q) || r.noRm.toLowerCase().includes(q) || r.dokter.toLowerCase().includes(q)
}
return true
})
})
const goEdit = (id: number | string) => {
router.replace({
path: route.path,
query: {
...route.query,
mode: 'entry',
'record-id': id,
},
})
}
const goView = (id: number | string) => {
router.replace({
path: route.path,
query: {
...route.query,
mode: 'view',
'record-id': id,
},
})
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
async function onGetDetail(id: number | string) {
isLoading.value = true
const res = sampleReport
recItem.value = res
console.log(res)
isLoading.value = false
}
// #region watcher
watch([recId, recAction], (newVal) => {
const [id, action] = newVal
// Guard: jangan proses jika id = 0 atau action kosong
if (!id || !action) return
switch (action) {
case ActionEvents.showDetail:
// onGetDetail(recId.value)
goView(id)
title.value = 'Detail Konsultasi'
break
case ActionEvents.showEdit:
goEdit(id)
title.value = 'Edit Konsultasi'
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
// Reset KEDUANYA menggunakan nextTick agar tidak trigger watcher lagi
nextTick(() => {
recId.value = 0
recAction.value = ''
})
})
// #endregion
</script>
<template>
<div class="mx-auto max-w-full">
<div class="border-b p-6">
<h1 class="text-2xl font-semibold">Asesmen Kebutuhan Edukasi</h1>
<p class="mt-1 text-sm text-gray-500">Manajemen asesmen kebutuhan edukasi pasien rawat jalan</p>
</div>
<div class="flex flex-wrap items-center gap-3 border-b p-4">
<div class="flex items-center gap-2">
<input
v-model="search"
placeholder="Cari Nama / No.RM"
class="w-64 rounded border px-3 py-2"
/>
</div>
<div class="flex items-center gap-2">
<input
v-model="dateFrom"
type="date"
class="rounded border px-3 py-2"
/>
<span class="text-sm text-gray-500">-</span>
<input
v-model="dateTo"
type="date"
class="rounded border px-3 py-2"
/>
<ButtonAction
preset="custom"
title="Filter List Asesmen"
label="Filter"
icon="i-lucide-filter"
@click="
() => {
isDialogOpen = true
}
"
/>
</div>
<div class="ml-auto flex items-center gap-2">
<ButtonAction
preset="custom"
title="Riwayat"
icon="i-lucide-history"
label="Riwayat"
@click="
() => {
isDialogOpen = true
}
"
/>
<ButtonAction
preset="add"
title="Tambah Asesmen"
icon="i-lucide-plus"
label="Asesmen"
@click="
() => {
goToEntry()
}
"
/>
</div>
</div>
<div class="overflow-x-auto p-4">
<AppAssessmentEducationList
:data="filtered"
:pagination-meta="{
recordCount: 2,
page: 1,
pageSize: 10,
totalPage: 1,
hasPrev: false,
hasNext: false,
}"
/>
</div>
</div>
<Dialog
v-model:open="isDialogOpen"
title="Arsip Riwayat Asesmen Kebutuhan Edukasi"
size="2xl"
prevent-outside
@update:open="
(value: any) => {
isDialogOpen = value
}
"
>
<AppAssessmentEducationListHistory
:data="filtered"
:pagination-meta="{
recordCount: 2,
page: 1,
pageSize: 10,
totalPage: 1,
hasPrev: false,
hasNext: false,
}"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="
() =>
handleActionRemove(
recItem.id,
() => {
router.go(0)
},
toast,
)
"
@cancel=""
>
<template #default="{ record }">
{{ console.log(JSON.stringify(record)) }}
<div class="space-y-1 text-sm">
<p
v-for="field in config.delKeyNames"
:key="field.key"
:v-if="record?.[field.key]"
>
<span class="font-semibold">{{ field.label }}:</span>
{{ record[field.key] }}
</p>
</div>
</template>
</RecordConfirmation>
</template>
@@ -0,0 +1,68 @@
export default {
operatorTeam: {
dpjpId: -1,
operatorName: 'Julian Alvarez',
assistantOperatorName: 'Arda Guller',
instrumentNurseName: 'Kenan Yildiz',
surgeryDate: '2025-11-13T14:29:00',
actionDiagnosis: 'Sprei gratisnya mana',
},
procedures: [
{
id: -1,
name: 'Ndase mumet',
code: 'CX1',
},
],
operationExecution: {
surgeryType: 'khusus',
billingCode: 'local',
operationSystem: 'cito',
surgeryCleanType: 'kotor',
surgeryNumber: 'retry',
birthPlaceNote: 'out3',
personWeight: 100,
operationDescription: 'asdsadsa1',
birthRemark: 'lahir_hidup',
operationStartAt: '2025-11-13T14:29:00',
operationEndAt: '2025-11-13T17:29:00',
anesthesiaStartAt: '2025-11-13T11:29:00',
anesthesiaEndAt: '2025-11-13T18:29:00',
},
bloodInput: {
type: 'tc',
amount: {
prc: null,
wb: null,
ffp: null,
tc: 3243324,
},
},
implant: {
brand: 'Samsung',
name: 'S.Komedi',
companionName: 'When ya',
},
specimen: {
destination: 'pa',
},
tissueNotes: [
{
note: 'Anjai',
},
{
note: 'Ciee Kaget',
},
{
note: 'Baper',
},
{
note: 'Saltink weeh',
},
{
note: 'Kaburrr',
},
],
}
@@ -0,0 +1,67 @@
<script setup lang="ts">
import mockData from './sample'
// types
import { type ActionReportFormData } from '~/schemas/action-report.schema'
import { type Encounter } from '~/models/encounter'
// Components
import AppActionReportPreview from '~/components/app/action-report/preview.vue'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
// #region Props & Emits
const router = useRouter()
const { backToList, goToEntry } = useQueryCRUDMode('mode')
const { recordId } = useQueryCRUDRecordId('record-id')
function onEditFromView() {
goToEntry({ fromView: true })
}
const props = defineProps<{
encounter: Encounter
}>()
// #endregion
// #region State & Computed
const reportData = ref<ActionReportFormData | null>(null)
const headerPrep: HeaderPrep = {
title: 'Detail Laporan Tindakan',
icon: 'i-lucide-stethoscope',
}
// #endregion
// #region Lifecycle Hooks
onMounted(async () => {
reportData.value = mockData as unknown as ActionReportFormData
})
// #endregion
// #region Functions
// #endregion region
// #region Utilities & event handlers
function onEdit() {
router.push({
name: 'action-report-id-edit',
params: { id: 100 },
})
}
function onBack() {}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<AppActionReportPreview
v-if="reportData"
:data="reportData"
@back="backToList"
@edit="onEditFromView"
/>
</template>
@@ -10,13 +10,14 @@ import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
import { type ControlLetter } from '~/models/control-letter'
// #region Props & Emits
const props = defineProps<{
const props = withDefaults(defineProps<{
encounter_id: number
callbackUrl?: string
}>()
}>(), {
})
// form related state
const route = useRoute()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const controlLetterForm = ref<ExposedForm<any> | null>(null)
// #endregion
@@ -72,7 +73,7 @@ async function composeFormData(): Promise<ControlLetter> {
if (!allValid) return Promise.reject('Form validation failed')
const formData = controlLetter?.values
formData.encounter_id = encounterId
formData.encounter_id = props.encounter_id
return new Promise((resolve) => resolve(formData))
}
// #endregion region
@@ -1,9 +1,6 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { withBase } from '~/models/_base'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
import type { Patient } from '~/models/patient'
import type { Person } from '~/models/person'
import { getDetail } from '~/services/control-letter.service'
// Components
@@ -11,18 +8,19 @@ import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import type { ControlLetter } from '~/models/control-letter'
// #region Props & Emits
const props = defineProps<{
}>()
const props = withDefaults(defineProps<{
encounter_id: number
record_id: number
redirectToForm?: (myRecord_id?: any) => void
}>(), {
redirectToForm: () => {},
})
// #endregion
// #region State & Computed
const route = useRoute()
const router = useRouter()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const controlLetterId = typeof route.params.control_letter_id == 'string' ? parseInt(route.params.control_letter_id) : 0
const controlLetter = ref<ControlLetter | null>(null)
const headerPrep: HeaderPrep = {
@@ -34,7 +32,7 @@ const headerPrep: HeaderPrep = {
// #region Lifecycle Hooks
onMounted(async () => {
const result = await getDetail(controlLetterId, {
const result = await getDetail(props.record_id, {
includes: "unit,specialist,subspecialist,doctor-employee-person",
})
if (result.success) {
@@ -54,11 +52,7 @@ function goBack() {
function handleAction(type: string) {
switch (type) {
case 'edit':
// TODO: Handle edit action
navigateTo({
name: 'rehab-encounter-id-control-letter-control_letter_id-edit',
params: { id: encounterId, "control_letter_id": controlLetterId },
})
props.redirectToForm(props.record_id)
break
case 'back':
+9 -20
View File
@@ -1,17 +1,7 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import type { Patient, genPatientProps } from '~/models/patient'
import type { ExposedForm } from '~/types/form'
import type { PatientBase } from '~/models/patient'
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
import { genPatient } from '~/models/patient'
import { PatientSchema } from '~/schemas/patient.schema'
import { PersonAddressRelativeSchema } from '~/schemas/person-address-relative.schema'
import { PersonAddressSchema } from '~/schemas/person-address.schema'
import { PersonContactListSchema } from '~/schemas/person-contact.schema'
import { PersonFamiliesSchema } from '~/schemas/person-family.schema'
import { ResponsiblePersonSchema } from '~/schemas/person-relative.schema'
import { uploadAttachment } from '~/services/patient.service'
import { getDetail, update } from '~/services/control-letter.service'
import {
@@ -26,27 +16,26 @@ import {
} from '~/handlers/control-letter.handler'
import { toast } from '~/components/pub/ui/toast'
import { withBase } from '~/models/_base'
import type { Person } from '~/models/person'
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
import type { ControlLetter } from '~/models/control-letter'
import { ControlLetterSchema } from '~/schemas/control-letter.schema'
import { formatDateYyyyMmDd } from '~/lib/date'
// #region Props & Emits
const props = defineProps<{
const props = withDefaults(defineProps<{
encounter_id: number
callbackUrl?: string
}>()
record_id: number
}>(), {
})
// form related state
const controlLetterForm = ref<ExposedForm<any> | null>(null)
// #endregion
// #region State & Computed
const route = useRoute()
const router = useRouter()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const controlLetterId = typeof route.params.control_letter_id == 'string' ? parseInt(route.params.control_letter_id) : 0
const isConfirmationOpen = ref(false)
const controlLetter = ref({})
@@ -58,7 +47,7 @@ const selectedSubSpecialistId = ref<number|null>(null)
// #region Lifecycle Hooks
onMounted(async () => {
const result = await getDetail(controlLetterId)
const result = await getDetail(props.record_id)
if (result.success) {
const responseData = {...result.body.data, date: formatDateYyyyMmDd(result.body.data.date)}
selectedUnitId.value = responseData?.unit_code
@@ -78,7 +67,7 @@ function goBack() {
async function handleConfirmAdd() {
const response = await handleActionEdit(
controlLetterId,
props.record_id,
await composeFormData(),
() => { },
() => { },
@@ -99,7 +88,7 @@ async function composeFormData(): Promise<ControlLetter> {
if (!allValid) return Promise.reject('Form validation failed')
const formData = controlLetter?.values
formData.encounter_id = encounterId
formData.encounter_id = props.encounter_id
return new Promise((resolve) => resolve(formData))
}
// #endregion region
+18 -19
View File
@@ -13,24 +13,28 @@ import type { Encounter } from '~/models/encounter'
import WarningAlert from '~/components/pub/my-ui/alert/warning-alert.vue'
// import type { PagePermission } from '~/models/role'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import { permissions } from '~/const/page-permission/chemoteraphy'
import { permissions } from '~/const/page-permission/ambulatory'
import { unauthorizedToast } from '~/lib/utils'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
import HistoryDialog from '~/components/app/control-letter/history-dialog.vue'
import type { RoleAccesses } from '~/models/role'
// #endregion
// #region Permission
const roleAccess = permissions['/rehab/encounter'] || {}
const roleAccess: RoleAccesses = permissions['/ambulatory/encounter'] || {}
const { getPagePermissions } = useRBAC()
const pagePermission = getPagePermissions(roleAccess)
// #region State
const props = defineProps<{
encounter?: Encounter
}>()
const route = useRoute()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const props = withDefaults(defineProps<{
encounter_id: number
redirectToForm?: (myRecord_id?: any) => void
redirectToDetail?: (myRecord_id: string|number) => void
}>(), {
redirectToForm: () => { },
redirectToDetail: () => { }
})
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
fetchFn: (params) => getList({ ...params, includes: 'specialist,subspecialist,doctor-employee-person', }),
@@ -73,10 +77,11 @@ const headerPrep: HeaderPrep = {
if (pagePermission.canCreate) {
headerPrep.addNav = {
label: "Surat Kontrol",
onClick: () => navigateTo({
name: 'rehab-encounter-id-control-letter-add',
params: { id: encounterId },
}),
onClick: () => {
console.log('redirectToForm')
console.log(props.redirectToForm())
props.redirectToForm()
},
}
}
// #endregion
@@ -136,18 +141,12 @@ provide('table_data_loader', isLoading)
watch([recId, recAction, timestamp], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
navigateTo({
name: 'rehab-encounter-id-control-letter-control_letter_id',
params: { id: encounterId, "control_letter_id": recId.value },
})
props.redirectToDetail(recId.value)
break
case ActionEvents.showEdit:
if(pagePermission.canUpdate){
navigateTo({
name: 'rehab-encounter-id-control-letter-control_letter_id-edit',
params: { id: encounterId, "control_letter_id": recId.value },
})
props.redirectToForm(recId.value)
} else {
unauthorizedToast()
}
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { crudQueryParamsMode } from '~/lib/system-constants';
import List from './list.vue'
import Detail from './detail.vue'
import Add from './add.vue'
import Edit from './edit.vue'
const props = defineProps<{
encounter_id: number
}>()
const { crudQueryParams, goToDetail, goToEntry } = useQueryCRUD()
</script>
<template>
<List v-if="crudQueryParams.mode === crudQueryParamsMode.list && !crudQueryParams.recordId"
:encounter_id="encounter_id"
:redirectToForm="goToEntry"
:redirectToDetail="goToDetail"/>
<Detail v-else-if="crudQueryParams.mode === crudQueryParamsMode.list && crudQueryParams.recordId"
:encounter_id="encounter_id"
:record_id="crudQueryParams.recordId"
:redirectToForm="goToEntry"/>
<Add v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry && !crudQueryParams.recordId"
:encounter_id="encounter_id" />
<Edit v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry && crudQueryParams.recordId"
:encounter_id="encounter_id"
:record_id="crudQueryParams.recordId" />
<List v-else :encounter_id="encounter_id" :redirectToForm="goToEntry" :redirectToDetail="goToDetail" />
</template>
+8 -20
View File
@@ -10,13 +10,13 @@ import { toFormData } from '~/lib/utils'
import { uploadAttachment } from '~/services/supporting-document.service'
// #region Props & Emits
const props = defineProps<{
callbackUrl?: string
}>()
const props = withDefaults(defineProps<{
encounter_id: number
}>(), {
})
// 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
@@ -43,20 +43,14 @@ async function handleConfirmAdd() {
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)
}
// const data = (response?.body?.data ?? null)
goBack()
}
async function composeFormData(): Promise<any> {
inputForm.value?.setValues({
...inputForm.value?.values,
ref_id: encounterId,
ref_id: props.encounter_id,
upload_employee_id: user.employee_id
})
@@ -83,13 +77,7 @@ async function handleActionClick(eventType: string) {
isConfirmationOpen.value = true
}
if (eventType === 'back') {
if (props.callbackUrl) {
await navigateTo(props.callbackUrl)
return
}
goBack()
}
if (eventType === 'back') goBack()
}
function handleCancelAdd() {
+10 -17
View File
@@ -8,15 +8,15 @@ 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
}>()
const props = withDefaults(defineProps<{
encounter_id: number
record_id: number
}>(), {
})
// 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 docId = props.record_id
const inputForm = ref<ExposedForm<any> | null>(null)
// #endregion
@@ -77,7 +77,7 @@ async function composeFormData(): Promise<any> {
if (!allValid) return Promise.reject('Form validation failed')
const formData = inputFormState?.values
formData.encounter_id = encounterId
formData.encounter_id = props.encounter_id
return new Promise((resolve) => resolve(formData))
}
// #endregion region
@@ -88,14 +88,7 @@ async function handleActionClick(eventType: string) {
isConfirmationOpen.value = true
}
if (eventType === 'back') {
if (props.callbackUrl) {
await navigateTo(props.callbackUrl)
return
}
goBack()
}
if (eventType === 'back') goBack()
}
function handleCancelAdd() {
@@ -109,7 +102,7 @@ function handleCancelAdd() {
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">
<h1>Upload Dokumen</h1>
<h1>Update Upload Dokumen</h1>
</div>
<AppDocumentUploadEntryForm
ref="inputForm"
+16 -23
View File
@@ -9,33 +9,30 @@ import type { Encounter } from '~/models/encounter'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import type { PagePermission } from '~/models/role'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import type { Permission, RoleAccesses, } from '~/models/role'
import { unauthorizedToast } from '~/lib/utils'
import { permissions } from '~/const/page-permission/ambulatory'
import { usePageChecker } from '~/lib/page-checker'
// #endregion
// #region Permission
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const roleAccess: RoleAccesses = permissions['/ambulatory/encounter'] || {}
const { getPagePermissions } = useRBAC()
const pagePermission = getPagePermissions(roleAccess)
// const {user,userRole} = useUserStore()
// const {getUserPermissions} = useRBAC()
// #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 props = withDefaults(defineProps<{
encounter_id: number
redirectToForm?: (myRecord_id?: any) => void
}>(), {
redirectToForm: () => { }
})
const { data, paginationMeta, handlePageChange, handleSearch, searchInput, fetchData } = usePaginatedList({
fetchFn: (params) => getList({
'encounter-id': encounterId,
'encounter-id': props.encounter_id,
// includes: "employee",
...params,
}),
@@ -56,10 +53,9 @@ const headerPrep: HeaderPrep = {
if (pagePermission.canCreate) {
headerPrep.addNav = {
label: "Upload Dokumen",
onClick: () => navigateTo({
name: 'rehab-encounter-id-document-upload-add',
params: { id: encounterId },
}),
onClick: () => {
props.redirectToForm()
},
}
}
@@ -78,6 +74,7 @@ const refSearchNav: RefSearchNav = {
// #region Lifecycle Hooks
onMounted(() => {
})
// #endregion
@@ -99,7 +96,6 @@ async function handleConfirmDelete(record: any, action: string) {
}
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
@@ -124,10 +120,7 @@ watch([recId, recAction, timestamp], () => {
case ActionEvents.showEdit:
if(pagePermission.canUpdate){
navigateTo({
name: 'rehab-encounter-id-document-upload-document_id-edit',
params: { id: encounterId, "document_id": recId.value },
})
props.redirectToForm(recId.value)
} else {
unauthorizedToast()
}
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { crudQueryParamsMode } from '~/lib/system-constants';
import List from './list.vue'
import Add from './add.vue'
import Edit from './edit.vue'
const props = defineProps<{
encounter_id: number
}>()
const { crudQueryParams, goToEntry } = useQueryCRUD()
</script>
<template>
<List v-if="crudQueryParams.mode === crudQueryParamsMode.list" :encounter_id="encounter_id" :redirectToForm="goToEntry" />
<Add v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry && !crudQueryParams.recordId" :encounter_id="encounter_id" />
<Edit v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry && crudQueryParams.recordId"
:encounter_id="encounter_id"
:record_id="crudQueryParams.recordId" />
<List v-else :encounter_id="encounter_id" :redirectToForm="goToEntry" />
</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
+2 -2
View File
@@ -142,10 +142,10 @@ async function getPatientList() {
try {
const params: any = { includes: includesParams, ...filterParams.value }
if (props.classCode) {
params.class_code = props.classCode
params['class-code'] = props.classCode
}
if (props.subClassCode) {
params.sub_class_code = props.subClassCode
params['sub-class-code'] = props.subClassCode
}
const result = await getEncounterList(params)
if (result.success) {
+17 -6
View File
@@ -29,8 +29,10 @@ import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
import Radiology from '~/components/content/radiology-order/main.vue'
import Consultation from '~/components/content/consultation/list.vue'
import Cprj from '~/components/content/cprj/entry.vue'
import ActionReport from '~/components/content/action-report/entry.vue'
import DocUploadList from '~/components/content/document-upload/list.vue'
import GeneralConsentList from '~/components/content/general-consent/entry.vue'
import SummaryMedic from '~/components/content/summary-medic/entry.vue'
import ResumeList from '~/components/content/resume/list.vue'
import ControlLetterList from '~/components/content/control-letter/list.vue'
import InitialNursingStudy from '~/components/content/initial-nursing/entry.vue'
@@ -56,7 +58,7 @@ const router = useRouter()
const { user, userActiveRole, getActiveRole } = useUserStore()
const activeRole = getActiveRole()
const activePosition = ref(getServicePosition(activeRole))
const menus = ref([] as any)
const menus = shallowRef([] as any)
const activeMenu = computed({
get: () => (route.query?.menu && typeof route.query.menu === 'string' ? route.query.menu : 'status'),
set: (value: string) => {
@@ -101,6 +103,13 @@ const protocolRows = [
{ value: 'therapy-protocol', label: 'Protokol Terapi' },
{ value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' },
{ value: 'patient-note', label: 'CPRJ', component: Cprj, props: { encounter: data } },
{ value: 'summary-medic', label: 'Profil Ringkasan Medis', component: SummaryMedic, props: { encounter: data } },
{ value: 'consent', label: 'General Consent', component: GeneralConsentList, props: { encounter: data } },
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.value.id } },
{ value: 'device-order', label: 'Order Alkes', component: DeviceOrder, props: { encounter_id: data.value.id } },
{ value: 'device', label: 'Order Alkes' },
{ 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: 'consent', label: 'General Consent', component: GeneralConsentList, props: { encounter: data } },
{
value: 'initial-nursing-study',
@@ -108,11 +117,6 @@ const protocolRows = [
component: InitialNursingStudy,
props: { encounter: data },
},
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.value.id } },
{ value: 'device-order', label: 'Order Alkes', component: DeviceOrder, props: { encounter_id: data.value.id } },
{ value: 'device', label: 'Order Alkes' },
{ 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' },
@@ -121,6 +125,13 @@ const protocolRows = [
{ value: 'resume', label: 'Resume', component: ResumeList, props: { encounter: data } },
{ value: 'control', label: 'Surat Kontrol', component: ControlLetterList, props: { encounter: data } },
{ value: 'screening', label: 'Skrinning MPP' },
{
value: 'report',
label: 'Laporan Tindakan',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: ActionReport,
props: { encounter: data },
},
{
value: 'supporting-document',
label: 'Upload Dokumen Pendukung',
@@ -117,7 +117,6 @@ async function actionHandler(type: string) {
})
}
console.log('data', result)
const resp = await handleActionSave(
{
...payload.value,
+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>
+10 -11
View File
@@ -10,17 +10,16 @@ import { getDetail } from '~/services/kfr.service';
// #region Props & Emits
const props = withDefaults(defineProps<{
encounter_id: number
callbackUrl?: string
mode?: 'add' | 'edit'
record_id: number
}>(), {
mode: "add",
})
// form related state
const route = useRoute()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const kfrId = typeof route.params.kfr_id == 'string' ? parseInt(route.params.kfr_id) : 0
const mode = computed(() => props.record_id ? `edit` : `add`)
const inputForm = ref<ExposedForm<any> | null>(null)
// #endregion
@@ -31,13 +30,13 @@ const isConfirmationOpen = ref(false)
// #region Lifecycle Hooks
onMounted(() => {
if(props.mode === `edit`) init()
if(mode.value === `edit`) init()
})
// #endregion
// #region Functions
async function init(){
const result = await getDetail(kfrId)
const result = await getDetail(props.record_id)
if (result.success) {
const currentValue = result.body?.data || {}
inputForm.value?.setValues(currentValue)
@@ -50,7 +49,7 @@ function goBack() {
async function handleConfirmAdd() {
const inputData: any = await composeFormData()
const response = props.mode === `add`
const response = mode.value === `add`
? await handleActionSave(
inputData,
() => { },
@@ -58,7 +57,7 @@ async function handleConfirmAdd() {
toast,
)
: await handleActionEdit(
kfrId,
props.record_id,
inputData,
() => { },
() => { },
@@ -82,7 +81,7 @@ async function composeFormData(): Promise<any> {
if (!allValid) return Promise.reject('Form validation failed')
const formData = input?.values
formData.encounter_id = encounterId
formData.encounter_id = props.encounter_id
return new Promise((resolve) => resolve(formData))
}
@@ -123,7 +122,7 @@ const initial = {
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">
{{ props.mode === "add" ? `Tambah` : `Update` }} Formulir Rawat Jalan KFR
Formulir Rawat Jalan KFR
</div>
<AppKfrEntry
ref="inputForm"
+11 -15
View File
@@ -27,10 +27,12 @@ import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
interface Props {
encounter: Encounter
}
const props = defineProps<Props>()
const route = useRoute()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const kfrId = typeof route.params.kfr_id == 'string' ? parseInt(route.params.kfr_id) : 0
const props = withDefaults(defineProps<{
encounter_id: number
redirectToForm?: (myRecord_id?: any) => void
}>(), {
redirectToForm: () => { }
})
// #endregion
// #region State
@@ -104,17 +106,16 @@ const addBtnTxt = computed(() => {
}
return `Tambah Asesmen`
})
const headerPrep: HeaderPrep = {
const headerPrep: HeaderPrep = {
title: "Formulir Rawat Jalan KFR",
icon: 'i-lucide-newspaper',
}
if(isDoctor.value || isAdmin.value) {
headerPrep.addNav = {
label: addBtnTxt.value,
onClick: () => navigateTo({
name: 'rehab-encounter-id-kfr-add',
}),
onClick: () => {
props.redirectToForm()
},
}
}
if(!isAssessment.value) {
@@ -283,12 +284,7 @@ watch([recId, recAction, timestamp], () => {
switch (recAction.value) {
case ActionEvents.showEdit:
// if(pagePermission.canUpdate) {
navigateTo({
name: 'rehab-encounter-id-kfr-kfr_id-edit',
params: {
kfr_id: kfrId
}
})
props.redirectToForm(recId.value)
// } else {
// unauthorizedToast()
// }
+23
View File
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { crudQueryParamsMode } from '~/lib/system-constants';
import List from './list.vue'
import Entry from './entry.vue'
const props = defineProps<{
encounter_id: number
}>()
const { crudQueryParams, goToDetail, goToEntry } = useQueryCRUD()
</script>
<template>
<List v-if="crudQueryParams.mode === crudQueryParamsMode.list"
:encounter_id="encounter_id"
:redirectToForm="goToEntry"
:redirectToDetail="goToDetail"/>
<Entry v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry"
:encounter_id="encounter_id"
:record_id="crudQueryParams.recordId" />
<List v-else :encounter_id="encounter_id" :redirectToForm="goToEntry" :redirectToDetail="goToDetail" />
</template>
@@ -131,6 +131,7 @@ async function getItems() {
<Detail :data="data" />
<div class="font-semibold pt-4 text-sm mb-1">Daftar Item</div>
<ItemListEntry
:data="items"
@requestItem="requestItem"/>
+6 -5
View File
@@ -11,17 +11,18 @@ import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import type { Prb } from '~/models/prb'
// #region Props & Emits
const props = defineProps<{
}>()
const props = withDefaults(defineProps<{
encounter_id: number
record_id: number
}>(), {
})
// #endregion
// #region State & Computed
const route = useRoute()
const router = useRouter()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const PrbId = typeof route.params.surgery_report_id == 'string' ? parseInt(route.params.surgery_report_id) : 0
const Prb = ref<Prb | null>(null)
const headerPrep: HeaderPrep = {
+6 -18
View File
@@ -4,21 +4,11 @@ import type { Patient, genPatientProps } from '~/models/patient'
import type { ExposedForm } from '~/types/form'
import type { PatientBase } from '~/models/patient'
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { genPatient } from '~/models/patient'
import { PatientSchema } from '~/schemas/patient.schema'
import { PersonAddressRelativeSchema } from '~/schemas/person-address-relative.schema'
import { PersonAddressSchema } from '~/schemas/person-address.schema'
import { PersonContactListSchema } from '~/schemas/person-contact.schema'
import { PersonFamiliesSchema } from '~/schemas/person-family.schema'
import { ResponsiblePersonSchema } from '~/schemas/person-relative.schema'
import { uploadAttachment } from '~/services/patient.service'
import { getDetail, update } from '~/services/prb.service'
import type { Prb } from '~/models/prb'
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
import { toast } from '~/components/pub/ui/toast'
import { withBase } from '~/models/_base'
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
import { PrbSchema } from '~/schemas/prb.schema'
import { formatDateYyyyMmDd } from '~/lib/date'
@@ -29,8 +19,10 @@ import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types
import { formatDateToDatetimeLocal } from '~/lib/utils'
// #region Props & Emits
const props = withDefaults(defineProps<{
const props = withDefaults(defineProps<{
encounter_id: number
callbackUrl?: string
record_id: number
isBpjs?: boolean
}>(), {
isBpjs: false,
@@ -44,11 +36,7 @@ const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSe
// #endregion
// #region State & Computed
const route = useRoute()
const router = useRouter()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const PrbId = typeof route.params.surgery_report_id == 'string' ? parseInt(route.params.surgery_report_id) : 0
const inputForm = ref<ExposedForm<any> | null>(null)
const Prb = ref({})
const isConfirmationOpen = ref(false)
@@ -60,7 +48,7 @@ provide("isSepDialogOpen", isSepDialogOpen);
// #region Lifecycle Hooks
onMounted(async () => {
const result = await getDetail(PrbId)
const result = await getDetail(props.record_id)
if (result.success) {
const responseData = {...result.body.data, date: formatDateYyyyMmDd(result.body.data.date)}
Prb.value = responseData
@@ -76,7 +64,7 @@ function goBack() {
async function handleConfirmAdd() {
const response = await handleActionEdit(
PrbId,
props.record_id,
await composeFormData(),
() => { },
() => { },
@@ -97,7 +85,7 @@ async function composeFormData(): Promise<Prb> {
if (!allValid) return Promise.reject('Form validation failed')
const formData = input?.values
formData.encounter_id = encounterId
formData.encounter_id = props.encounter_id
return new Promise((resolve) => resolve(formData))
}
// #endregion region
+10 -11
View File
@@ -19,13 +19,16 @@ import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
// #region State
const props = withDefaults(defineProps<{
encounter?: Encounter
encounter_id: number
isBpjs?: boolean
redirectToForm?: (myRecord_id?: any) => void
redirectToDetail?: (myRecord_id: string|number) => void
}>(), {
isBpjs: false,
redirectToForm: () => { },
redirectToDetail: () => { }
})
const route = useRoute()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const { user } = useUserStore()
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
fetchFn: (params) => getList({ ...params, includes: '', }),
@@ -66,10 +69,9 @@ const headerPrep: HeaderPrep = {
if(true){
headerPrep.addNav = {
label: "Program Rujuk Balik",
onClick: () => navigateTo({
name: 'rehab-encounter-id-prb-add',
params: { id: encounterId },
}),
onClick: () => {
props.redirectToForm()
},
};
}
headerPrep.components = [
@@ -134,10 +136,7 @@ provide('isHistoryDialogOpen', isHistoryDialogOpen)
watch([recId, recAction, timestamp], () => {
switch (recAction.value) {
case ActionEvents.showEdit:
navigateTo({
name: 'rehab-encounter-id-prb-prb_id-edit',
params: { id: encounterId, "prb_id": recId.value },
})
props.redirectToForm(recId.value)
break
case ActionEvents.showPrint:
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { crudQueryParamsMode } from '~/lib/system-constants';
import List from './list.vue'
import Detail from './detail.vue'
import Entry from './entry.vue'
const props = defineProps<{
encounter_id: number
}>()
const { crudQueryParams, goToDetail, goToEntry } = useQueryCRUD()
</script>
<template>
<List v-if="crudQueryParams.mode === crudQueryParamsMode.list && !crudQueryParams.recordId"
:encounter_id="encounter_id"
:redirectToForm="goToEntry"
:redirectToDetail="goToDetail"/>
<Detail v-else-if="crudQueryParams.mode === crudQueryParamsMode.list && crudQueryParams.recordId"
:encounter_id="encounter_id"
:record_id="crudQueryParams.recordId"
:redirectToForm="goToEntry"/>
<Entry v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry"
:encounter_id="encounter_id"
:record_id="crudQueryParams.recordId" />
<List v-else :encounter_id="encounter_id" :redirectToForm="goToEntry" :redirectToDetail="goToDetail" />
</template>
@@ -132,9 +132,20 @@ async function getItems() {
<Detail :data="data" />
<div class="mb-5">
<div>Keterangan Klinis</div>
<Textarea />
</div>
<ItemListEntry
:data="items"
@requestItem="requestItem"/>
<div class="my-5">
<div>Catatan Pemeriksaan DSA</div>
<Textarea />
</div>
<Separator class="my-5" />
<div class="w-full flex justify-center">
+6 -2
View File
@@ -13,9 +13,13 @@ import FarmacyHistoryDialog from '~/components/app/resume/history-list/farmacy-h
import NationalProgramHistoryDialog from '~/components/app/resume/history-list/national-program-history-dialog.vue';
// #region Props & Emits
const props = defineProps<{
const props = withDefaults(defineProps<{
encounter_id: number
callbackUrl?: string
}>()
record_id: number
}>(), {
})
// form related state
const personPatientForm = ref<ExposedForm<any> | null>(null)
+16 -3
View File
@@ -19,18 +19,27 @@ import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
import type { ExposedForm } from '~/types/form'
import { VerificationSchema } from '~/schemas/verification.schema'
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
import type { PagePermission } from '~/models/role'
import type { RoleAccesses } from '~/models/role'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import { unauthorizedToast } from '~/lib/utils'
import { permissions } from '~/const/page-permission/ambulatory'
// #endregion
// #region Permission
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const roleAccess: RoleAccesses = permissions['/ambulatory/encounter'] || {}
const { getPagePermissions } = useRBAC()
const pagePermission = getPagePermissions(roleAccess)
// #endregion
// #region State
const props = withDefaults(defineProps<{
encounter_id: number
redirectToForm?: (myRecord_id?: any) => void
}>(), {
redirectToForm: () => { }
})
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
entityName: 'patient',
@@ -67,7 +76,9 @@ const headerPrep: HeaderPrep = {
if (pagePermission.canCreate) {
headerPrep.addNav = {
label: "Resume",
onClick: () => navigateTo('/resume/add'),
onClick: () => {
props.redirectToForm()
},
}
}
// #endregion
@@ -158,6 +169,7 @@ provide('table_data_loader', isLoading)
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showVerify:
isVerifyDialogOpen.value = true
if(pagePermission.canUpdate) {
isVerifyDialogOpen.value = true
} else {
@@ -165,6 +177,7 @@ watch([recId, recAction], () => {
}
break
case ActionEvents.showValidate:
isRecordConfirmationOpen.value = true
if(pagePermission.canUpdate) {
isRecordConfirmationOpen.value = true
} else {

Some files were not shown because too many files have changed in this diff Show More