Merge branch 'dev' into feat/device-order
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' width='250' height='30' viewBox='0 0 1000 120'><rect fill='#000000' width='1000' height='120'/><g fill='none' stroke='#222' stroke-width='10' stroke-opacity='1'><path d='M-500 75c0 0 125-30 250-30S0 75 0 75s125 30 250 30s250-30 250-30s125-30 250-30s250 30 250 30s125 30 250 30s250-30 250-30'/><path d='M-500 45c0 0 125-30 250-30S0 45 0 45s125 30 250 30s250-30 250-30s125-30 250-30s250 30 250 30s125 30 250 30s250-30 250-30'/><path d='M-500 105c0 0 125-30 250-30S0 105 0 105s125 30 250 30s250-30 250-30s125-30 250-30s250 30 250 30s125 30 250 30s250-30 250-30'/><path d='M-500 15c0 0 125-30 250-30S0 15 0 15s125 30 250 30s250-30 250-30s125-30 250-30s250 30 250 30s125 30 250 30s250-30 250-30'/><path d='M-500-15c0 0 125-30 250-30S0-15 0-15s125 30 250 30s250-30 250-30s125-30 250-30s250 30 250 30s125 30 250 30s250-30 250-30'/><path d='M-500 135c0 0 125-30 250-30S0 135 0 135s125 30 250 30s250-30 250-30s125-30 250-30s250 30 250 30s125 30 250 30s250-30 250-30'/></g></svg>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { docTypeCode, supportingDocOpt, type docTypeCodeKey } from '~/lib/constants'
|
||||||
|
import { getValueLabelList as getDoctorLabelList } from '~/services/doctor.service'
|
||||||
|
import { getValueLabelList as getUnitLabelList } from '~/services/unit.service'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
import type { Item } from '~/components/pub/my-ui/combobox'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="supportingDocOpt"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import { Form } from '~/components/pub/ui/form'
|
||||||
|
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
import FileField from '~/components/pub/my-ui/form/file-field.vue'
|
||||||
|
import SelectDocType from './_common/select-doc-type.vue'
|
||||||
|
import Separator from '~/components/pub/ui/separator/Separator.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
schema: any
|
||||||
|
initialValues?: any
|
||||||
|
errors?: FormErrors
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formSchema = toTypedSchema(props.schema)
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
validate: () => formRef.value?.validate(),
|
||||||
|
resetForm: () => formRef.value?.resetForm(),
|
||||||
|
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
|
||||||
|
values: computed(() => formRef.value?.values),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
v-slot="{ values }"
|
||||||
|
as=""
|
||||||
|
keep-values
|
||||||
|
:validation-schema="formSchema"
|
||||||
|
:validate-on-mount="false"
|
||||||
|
validation-mode="onSubmit"
|
||||||
|
:initial-values="initialValues ? initialValues : {}"
|
||||||
|
>
|
||||||
|
<DE.Block :col-count="2" :cell-flex="false">
|
||||||
|
<InputBase
|
||||||
|
field-name="officer"
|
||||||
|
label="Petugas Upload"
|
||||||
|
placeholder="Masukkan Petugas Upload"
|
||||||
|
:is-disabled="true"
|
||||||
|
/>
|
||||||
|
<DE.Cell :col-span="2">
|
||||||
|
<Separator class="w-full my-4"/>
|
||||||
|
<DE.Block :col-count="3" :cell-flex="false">
|
||||||
|
<InputBase
|
||||||
|
field-name="name"
|
||||||
|
label="Nama Dokumen"
|
||||||
|
placeholder="Maukkan Nama Dokumen"
|
||||||
|
/>
|
||||||
|
<SelectDocType
|
||||||
|
field-name="type_code"
|
||||||
|
label="Tipe Dokumen"
|
||||||
|
placeholder="Pilih Jenis Dokumen"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<FileField
|
||||||
|
field-name="content"
|
||||||
|
label="Upload Dokumen"
|
||||||
|
placeholder="Unggah dokumen"
|
||||||
|
:errors="errors"
|
||||||
|
:accept="['pdf', 'jpg', 'png']"
|
||||||
|
:max-size-mb="1"
|
||||||
|
/>
|
||||||
|
</DE.Block>
|
||||||
|
</DE.Cell>
|
||||||
|
</DE.Block>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
import { docTypeCode, docTypeLabel, type docTypeCodeKey } from '~/lib/constants'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dd.vue'))
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{}, {}, {}, {width: 50},],
|
||||||
|
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'Nama Dokumen' },
|
||||||
|
{ label: 'Tipe Dokumen' },
|
||||||
|
{ label: 'Petugas Upload' },
|
||||||
|
{ label: 'Action' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keys: ['fileName', 'type_code', 'employee.name', 'action'],
|
||||||
|
|
||||||
|
delKeyNames: [
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
parses: {
|
||||||
|
type_code: (v: unknown) => {
|
||||||
|
return docTypeLabel[v?.type_code as docTypeCodeKey]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
return {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||||
|
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||||
|
import { config } from './list.cfg'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
:skeleton-size="paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<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 [responsibleName, responsibleNameAttrs] = defineField('responsibleName')
|
||||||
|
const [responsiblePhone, responsiblePhoneAttrs] = defineField('responsiblePhone')
|
||||||
|
const [informant, informantAttrs] = defineField('informant')
|
||||||
|
const [witness1, witness1Attrs] = defineField('witness1')
|
||||||
|
const [witness2, witness2Attrs] = defineField('witness2')
|
||||||
|
const [tanggal, tanggalAttrs] = defineField('tanggal')
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
<Cell>
|
||||||
|
<Label dynamic>Tanggal</Label>
|
||||||
|
<Field>
|
||||||
|
<Input
|
||||||
|
v-model="tanggal"
|
||||||
|
v-bind="tanggalAttrs"
|
||||||
|
:disabled="props.isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<Separator class="mt-8" />
|
||||||
|
|
||||||
|
<div class="my-2 flex items-center justify-between">
|
||||||
|
<h1 class="font-semibold">Anggota Keluarga</h1>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
@click="addRelative"
|
||||||
|
>
|
||||||
|
+ Tambah
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in relatives"
|
||||||
|
:key="idx"
|
||||||
|
class="my-2 rounded-md border border-slate-300 p-4"
|
||||||
|
>
|
||||||
|
<Block :colCount="2">
|
||||||
|
<Cell>
|
||||||
|
<Label dynamic>Nama Anggota Keluarga</Label>
|
||||||
|
<Field>
|
||||||
|
<Input v-model="relatives[idx].name" />
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
|
||||||
|
<Cell>
|
||||||
|
<Label dynamic>No. Hp Anggota Keluarga</Label>
|
||||||
|
<Field>
|
||||||
|
<Input v-model="relatives[idx].phone" />
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mt-3 text-sm text-red-500"
|
||||||
|
@click="removeRelative(idx)"
|
||||||
|
>
|
||||||
|
Hapus
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator class="mt-8" />
|
||||||
|
|
||||||
|
<!-- Responsible Section -->
|
||||||
|
<div class="my-2">
|
||||||
|
<h1 class="font-semibold">Penanggung Jawab</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
<!-- Informant -->
|
||||||
|
<div class="my-2">
|
||||||
|
<h1 class="font-semibold">Pemberi Informasi</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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,
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import Block from '~/components/pub/my-ui/doc-entry/block.vue'
|
||||||
|
import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
|
||||||
|
import Field from '~/components/pub/my-ui/doc-entry/field.vue'
|
||||||
|
import Label from '~/components/pub/my-ui/doc-entry/label.vue'
|
||||||
|
import Button from '~/components/pub/ui/button/Button.vue'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { BaseFormData } from '~/schemas/base.schema'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import type z from 'zod'
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
schema?: z.ZodSchema<any>
|
||||||
|
values?: any
|
||||||
|
isLoading?: boolean
|
||||||
|
isReadonly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const isLoading = props.isLoading !== undefined ? props.isLoading : false
|
||||||
|
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [values: BaseFormData, resetForm: () => void]
|
||||||
|
cancel: [resetForm: () => void]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { defineField, errors, meta } = useForm({
|
||||||
|
validationSchema: props.schema ? toTypedSchema(props.schema) : undefined,
|
||||||
|
initialValues: {
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const [code, codeAttrs] = defineField('code')
|
||||||
|
const [name, nameAttrs] = defineField('name')
|
||||||
|
|
||||||
|
if (props.values) {
|
||||||
|
if (props.values.code !== undefined) code.value = props.values.code
|
||||||
|
if (props.values.name !== undefined) name.value = props.values.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
code.value = ''
|
||||||
|
name.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmitForm() {
|
||||||
|
const formData = {
|
||||||
|
name: name.value || '',
|
||||||
|
code: code.value || '',
|
||||||
|
}
|
||||||
|
emit('submit', formData, resetForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancelForm() {
|
||||||
|
emit('cancel', resetForm)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
id="form-medicine-group"
|
||||||
|
@submit.prevent
|
||||||
|
>
|
||||||
|
<Block
|
||||||
|
labelSize="thin"
|
||||||
|
class="!mb-2.5 !pt-0 xl:!mb-3"
|
||||||
|
:colCount="1"
|
||||||
|
>
|
||||||
|
<Cell>
|
||||||
|
<Label height="">Kode</Label>
|
||||||
|
<Field :errMessage="errors.code">
|
||||||
|
<Input
|
||||||
|
id="code"
|
||||||
|
v-model="code"
|
||||||
|
v-bind="codeAttrs"
|
||||||
|
:disabled="isLoading || isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
<Label height="compact">Nama</Label>
|
||||||
|
<Field :errMessage="errors.name">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
v-model="name"
|
||||||
|
v-bind="nameAttrs"
|
||||||
|
:disabled="isLoading || isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
<div class="my-2 flex justify-end gap-2 py-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
class="w-[120px]"
|
||||||
|
@click="onCancelForm"
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="!isReadonly"
|
||||||
|
type="button"
|
||||||
|
class="w-[120px]"
|
||||||
|
:disabled="isLoading || !meta.valid"
|
||||||
|
@click="onSubmitForm"
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{}, {}, { width: 50 }],
|
||||||
|
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'Kode' },
|
||||||
|
{ label: 'Nama' },
|
||||||
|
{ label: 'Aksi' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keys: ['code', 'name', 'action'],
|
||||||
|
|
||||||
|
delKeyNames: [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
],
|
||||||
|
|
||||||
|
parses: {},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {},
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||||
|
|
||||||
|
// Configs
|
||||||
|
import { config } from './list-cfg'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -18,6 +18,7 @@ interface Props {
|
|||||||
isReadonly?: boolean
|
isReadonly?: boolean
|
||||||
medicineGroups?: { value: string; label: string }[]
|
medicineGroups?: { value: string; label: string }[]
|
||||||
medicineMethods?: { value: string; label: string }[]
|
medicineMethods?: { value: string; label: string }[]
|
||||||
|
medicineForms?: { value: string; label: string }[]
|
||||||
uoms?: { value: string; label: string }[]
|
uoms?: { value: string; label: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ const { defineField, errors, meta } = useForm({
|
|||||||
name: '',
|
name: '',
|
||||||
medicineGroup_code: '',
|
medicineGroup_code: '',
|
||||||
medicineMethod_code: '',
|
medicineMethod_code: '',
|
||||||
|
medicineForm_code: '',
|
||||||
uom_code: '',
|
uom_code: '',
|
||||||
stock: 0,
|
stock: 0,
|
||||||
},
|
},
|
||||||
@@ -45,6 +47,7 @@ const [code, codeAttrs] = defineField('code')
|
|||||||
const [name, nameAttrs] = defineField('name')
|
const [name, nameAttrs] = defineField('name')
|
||||||
const [medicineGroup_code, medicineGroupAttrs] = defineField('medicineGroup_code')
|
const [medicineGroup_code, medicineGroupAttrs] = defineField('medicineGroup_code')
|
||||||
const [medicineMethod_code, medicineMethodAttrs] = defineField('medicineMethod_code')
|
const [medicineMethod_code, medicineMethodAttrs] = defineField('medicineMethod_code')
|
||||||
|
const [medicineForm_code, medicineFormAttrs] = defineField('medicineForm_code')
|
||||||
const [uom_code, uomAttrs] = defineField('uom_code')
|
const [uom_code, uomAttrs] = defineField('uom_code')
|
||||||
const [stock, stockAttrs] = defineField('stock')
|
const [stock, stockAttrs] = defineField('stock')
|
||||||
|
|
||||||
@@ -53,6 +56,7 @@ if (props.values) {
|
|||||||
if (props.values.name !== undefined) name.value = props.values.name
|
if (props.values.name !== undefined) name.value = props.values.name
|
||||||
if (props.values.medicineGroup_code !== undefined) medicineGroup_code.value = props.values.medicineGroup_code
|
if (props.values.medicineGroup_code !== undefined) medicineGroup_code.value = props.values.medicineGroup_code
|
||||||
if (props.values.medicineMethod_code !== undefined) medicineMethod_code.value = props.values.medicineMethod_code
|
if (props.values.medicineMethod_code !== undefined) medicineMethod_code.value = props.values.medicineMethod_code
|
||||||
|
if (props.values.medicineForm_code !== undefined) medicineForm_code.value = props.values.medicineForm_code
|
||||||
if (props.values.uom_code !== undefined) uom_code.value = props.values.uom_code
|
if (props.values.uom_code !== undefined) uom_code.value = props.values.uom_code
|
||||||
if (props.values.stock !== undefined) stock.value = props.values.stock
|
if (props.values.stock !== undefined) stock.value = props.values.stock
|
||||||
}
|
}
|
||||||
@@ -62,6 +66,7 @@ const resetForm = () => {
|
|||||||
name.value = ''
|
name.value = ''
|
||||||
medicineGroup_code.value = ''
|
medicineGroup_code.value = ''
|
||||||
medicineMethod_code.value = ''
|
medicineMethod_code.value = ''
|
||||||
|
medicineForm_code.value = '',
|
||||||
uom_code.value = ''
|
uom_code.value = ''
|
||||||
stock.value = 0
|
stock.value = 0
|
||||||
}
|
}
|
||||||
@@ -72,6 +77,7 @@ function onSubmitForm() {
|
|||||||
name: name.value || '',
|
name: name.value || '',
|
||||||
medicineGroup_code: medicineGroup_code.value || '',
|
medicineGroup_code: medicineGroup_code.value || '',
|
||||||
medicineMethod_code: medicineMethod_code.value || '',
|
medicineMethod_code: medicineMethod_code.value || '',
|
||||||
|
medicineForm_code: medicineForm_code.value || '',
|
||||||
uom_code: uom_code.value || '',
|
uom_code: uom_code.value || '',
|
||||||
stock: stock.value || 0,
|
stock: stock.value || 0,
|
||||||
}
|
}
|
||||||
@@ -138,6 +144,20 @@ function onCancelForm() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</Cell>
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
<Label height="compact">Sediaan Obat</Label>
|
||||||
|
<Field :errMessage="errors.medicineForm_code">
|
||||||
|
<Select
|
||||||
|
id="medicineForm_code"
|
||||||
|
v-model="medicineForm_code"
|
||||||
|
icon-name="i-lucide-chevron-down"
|
||||||
|
placeholder="Pilih metode pemberian"
|
||||||
|
v-bind="medicineFormAttrs"
|
||||||
|
:items="props.medicineForms || []"
|
||||||
|
:disabled="isLoading || isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
<Cell>
|
<Cell>
|
||||||
<Label>Satuan</Label>
|
<Label>Satuan</Label>
|
||||||
<Field :errMessage="errors.uom_code">
|
<Field :errMessage="errors.uom_code">
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ export const config: Config = {
|
|||||||
{ label: 'Golongan' },
|
{ label: 'Golongan' },
|
||||||
{ label: 'Metode Pemberian' },
|
{ label: 'Metode Pemberian' },
|
||||||
{ label: 'Satuan' },
|
{ label: 'Satuan' },
|
||||||
|
{ label: 'Sediaan' },
|
||||||
{ label: 'Stok' },
|
{ label: 'Stok' },
|
||||||
{ label: 'Aksi' },
|
{ label: 'Aksi' },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
keys: ['code', 'name', 'group', 'method', 'unit', 'stock', 'action'],
|
keys: ['code', 'name', 'group', 'method', 'unit', 'form', 'stock', 'action'],
|
||||||
|
|
||||||
delKeyNames: [
|
delKeyNames: [
|
||||||
{ key: 'code', label: 'Kode' },
|
{ key: 'code', label: 'Kode' },
|
||||||
@@ -37,6 +38,9 @@ export const config: Config = {
|
|||||||
unit: (rec: unknown): unknown => {
|
unit: (rec: unknown): unknown => {
|
||||||
return (rec as SmallDetailDto).uom?.name || '-'
|
return (rec as SmallDetailDto).uom?.name || '-'
|
||||||
},
|
},
|
||||||
|
form: (rec: unknown): unknown => {
|
||||||
|
return (rec as SmallDetailDto).medicineForm?.name || '-'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ListItemDto } from '~/components/pub/my-ui/data/types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
url: string
|
||||||
|
btnTxt?: string
|
||||||
|
rec: ListItemDto
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePrint() {
|
||||||
|
navigateTo(props.url || 'https://google.com', {external: true,open: { target: "_blank" },});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
class="gap-3 items-center border-orange-400 text-orange-400"
|
||||||
|
variant="outline" @click="handlePrint">
|
||||||
|
<Icon name="i-lucide-printer" class="h-4 w-4" />
|
||||||
|
{{ props.btnTxt || 'Lampiran' }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const arrangementTypeOpts = [
|
||||||
|
{ label: 'KRS', value: "krs" },
|
||||||
|
{ label: 'MRS', value: "mrs" },
|
||||||
|
{ label: 'Rujuk Internal', value: "rujukInternal" },
|
||||||
|
{ label: 'Rujuk External', value: "rujukExternal" },
|
||||||
|
{ label: 'Meninggal', value: "meninggal" },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
v-show="label"
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField :name="fieldName" v-slot="{ componentField, value }">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
v-bind="componentField"
|
||||||
|
:model-value="value"
|
||||||
|
:items="arrangementTypeOpts"
|
||||||
|
:defaultValue='arrangementTypeOpts[0]?.value'
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import { differenceInDays, differenceInMonths, differenceInYears, parseISO } from 'date-fns'
|
||||||
|
import { Input } from '~/components/pub/ui/input'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
isDisabled?: boolean
|
||||||
|
isWithTime?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'birthDate',
|
||||||
|
label = 'Tanggal Lahir',
|
||||||
|
placeholder = 'Pilih tanggal lahir',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
isWithTime = false,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Reactive variables for age calculation
|
||||||
|
const patientAge = ref<string>('Masukkan tanggal lahir')
|
||||||
|
|
||||||
|
// Function to calculate age with years, months, and days
|
||||||
|
function calculateAge(birthDate: string | Date | undefined): string {
|
||||||
|
if (!birthDate) {
|
||||||
|
return 'Masukkan tanggal lahir'
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let dateObj: Date
|
||||||
|
|
||||||
|
if (typeof birthDate === 'string') {
|
||||||
|
dateObj = parseISO(birthDate)
|
||||||
|
} else {
|
||||||
|
dateObj = birthDate
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
|
||||||
|
// Calculate years, months, and days
|
||||||
|
const totalYears = differenceInYears(today, dateObj)
|
||||||
|
|
||||||
|
// Calculate remaining months after years
|
||||||
|
const yearsPassed = new Date(dateObj)
|
||||||
|
yearsPassed.setFullYear(yearsPassed.getFullYear() + totalYears)
|
||||||
|
const remainingMonths = differenceInMonths(today, yearsPassed)
|
||||||
|
|
||||||
|
// Calculate remaining days after years and months
|
||||||
|
const monthsPassed = new Date(yearsPassed)
|
||||||
|
monthsPassed.setMonth(monthsPassed.getMonth() + remainingMonths)
|
||||||
|
const remainingDays = differenceInDays(today, monthsPassed)
|
||||||
|
|
||||||
|
// Format the result
|
||||||
|
const parts = []
|
||||||
|
if (totalYears > 0) parts.push(`${totalYears} Tahun`)
|
||||||
|
if (remainingMonths > 0) parts.push(`${remainingMonths} Bulan`)
|
||||||
|
if (remainingDays > 0) parts.push(`${remainingDays} Hari`)
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join(' ') : '0 Hari'
|
||||||
|
} catch {
|
||||||
|
return 'Masukkan tanggal lahir'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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 && !isDisabled"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
id="birthDate"
|
||||||
|
:type="isWithTime ? 'datetime-local' : 'date'"
|
||||||
|
min="1900-01-01"
|
||||||
|
:max="new Date().toISOString().split('T')[0]"
|
||||||
|
v-bind="componentField"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
@update:model-value="
|
||||||
|
(value: string | number) => {
|
||||||
|
const dateStr = typeof value === 'number' ? String(value) : value
|
||||||
|
patientAge = calculateAge(dateStr)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
v-show="label"
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label = 'Pekerjaan',
|
||||||
|
placeholder = 'Pilih pekerjaan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
import { occupationCodes } from '~/lib/constants'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'job',
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
v-show="label"
|
||||||
|
:label-for="fieldName"
|
||||||
|
:class="cn('select-field-label', labelClass)"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
class="focus:ring-0 focus:ring-offset-0"
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="jobOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
search-placeholder="Cari..."
|
||||||
|
empty-message="Data tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Badge, type Variants } from '~/components/pub/ui/badge'
|
||||||
|
|
||||||
|
const activeStatusCodes: Record<string, string> = {
|
||||||
|
verified: 'Terverifikasi',
|
||||||
|
validated: 'Tervalidasi',
|
||||||
|
unverified: 'Belum Verifikasi',
|
||||||
|
unvalidated: 'Batal Validasi',
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rec: any
|
||||||
|
idx?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
let code: keyof typeof activeStatusCodes = `unverified`
|
||||||
|
switch (props.rec.status_code) {
|
||||||
|
case 1:
|
||||||
|
code = 'verified'
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
code = 'validated'
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
code = 'unverified'
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
code = 'unvalidated'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
code = 'unverified'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return activeStatusCodes[code]
|
||||||
|
})
|
||||||
|
|
||||||
|
const badgeVariant = computed(() => {
|
||||||
|
let variant: Variants = `outline`
|
||||||
|
switch (props.rec.status_code) {
|
||||||
|
case 1:
|
||||||
|
variant = 'secondary'
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
variant = 'default'
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
variant = 'outline'
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
variant = 'destructive'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
variant = 'outline'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return variant
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<Badge :variant="badgeVariant" class="rounded-2xl text-[0.6rem]" >
|
||||||
|
{{ statusText }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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 SelectDate from './_common/select-date.vue'
|
||||||
|
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'
|
||||||
|
import SelectArrangement from './_common/select-arrangement.vue'
|
||||||
|
import type { ResumeArrangementType } from '~/schemas/resume.schema'
|
||||||
|
import SelectFaskes from './_common/select-faskes.vue'
|
||||||
|
import SelectDeathCause from './_common/select-death-cause.vue'
|
||||||
|
import SelectIcd10 from './_common/select-icd-10.vue'
|
||||||
|
import SelectIcd9 from './_common/select-icd-9.vue'
|
||||||
|
import SelectConciousLevel from './_common/select-concious-level.vue'
|
||||||
|
import SelectPainScale from './_common/select-pain-scale.vue'
|
||||||
|
import SelectNationalProgramService from './_common/select-national-program-service.vue'
|
||||||
|
import SelectNationalProgramServiceStatus from './_common/select-national-program-service-status.vue'
|
||||||
|
import SelectHospitalLeaveCondition from './_common/select-hospital-leave-condition.vue'
|
||||||
|
import SelectHospitalLeaveMethod from './_common/select-hospital-leave-method.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
schema: any
|
||||||
|
initialValues?: any
|
||||||
|
resumeArrangementType: ResumeArrangementType
|
||||||
|
errors?: FormErrors
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isActionHistoryOpen = inject(`isActionHistoryOpen`) as Ref<boolean>
|
||||||
|
const isConsultationHistoryOpen = inject(`isConsultationHistoryOpen`) as Ref<boolean>
|
||||||
|
const isSupportingHistoryOpen = inject(`isSupportingHistoryOpen`) as Ref<boolean>
|
||||||
|
const isFarmacyHistoryOpen = inject(`isFarmacyHistoryOpen`) as Ref<boolean>
|
||||||
|
const isNationalProgramServiceHistoryOpen = inject(`isNationalProgramServiceHistoryOpen`) as Ref<boolean>
|
||||||
|
const formSchema = toTypedSchema(props.schema)
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
validate: () => formRef.value?.validate(),
|
||||||
|
resetForm: () => formRef.value?.resetForm(),
|
||||||
|
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
|
||||||
|
values: computed(() => formRef.value?.values),
|
||||||
|
})
|
||||||
|
|
||||||
|
const DEFAULT_SECONDARY_DIAGNOSIS_VALUE = {
|
||||||
|
diagnosis: '',
|
||||||
|
icd10: '',
|
||||||
|
diagnosisBasis: '',
|
||||||
|
};
|
||||||
|
const DEFAULT_SECONDARY_ACTION_VALUE = {
|
||||||
|
action: '',
|
||||||
|
icd9: '',
|
||||||
|
actionBasis: '',
|
||||||
|
};
|
||||||
|
const DEFAULT_CONSULTATION_VALUE = {
|
||||||
|
consultation: '',
|
||||||
|
consultationReply: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialFormValues = {
|
||||||
|
secondaryDiagnosis: [DEFAULT_SECONDARY_DIAGNOSIS_VALUE],
|
||||||
|
secondaryOperativeNonOperativeAct: [DEFAULT_SECONDARY_ACTION_VALUE],
|
||||||
|
consultation: [DEFAULT_CONSULTATION_VALUE],
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form ref="formRef"
|
||||||
|
v-slot="{ values }"
|
||||||
|
as=""
|
||||||
|
keep-values
|
||||||
|
:validation-schema="formSchema"
|
||||||
|
:validate-on-mount="false"
|
||||||
|
validation-mode="onSubmit"
|
||||||
|
:initial-values="initialValues ? initialValues : initialFormValues">
|
||||||
|
|
||||||
|
<!-- Pasien -->
|
||||||
|
<h1 class="mb-3 text-base font-medium">Pemeriksaan Pasien</h1>
|
||||||
|
<DE.Block :col-count="3" :cell-flex="false">
|
||||||
|
<SelectDate field-name="inDate" label="Tanggal Masuk" :errors="errors" :is-disabled="true" />
|
||||||
|
<SelectDate field-name="outDate" label="Tanggal Keluar" :errors="errors" :is-disabled="true" />
|
||||||
|
<InputBase
|
||||||
|
field-name="doctor_id"
|
||||||
|
:is-disabled="true"
|
||||||
|
label="DPJP" placeholder="DPJP"/>
|
||||||
|
</DE.Block>
|
||||||
|
<DE.Block class="" :cell-flex="false">
|
||||||
|
<InputBase
|
||||||
|
class="w-2/3"
|
||||||
|
field-name="first_diagnosis"
|
||||||
|
:is-disabled="true"
|
||||||
|
label="Diagnosis Masuk/Rujukan" placeholder="Diagnosis Masuk/Rujukan"/>
|
||||||
|
</DE.Block>
|
||||||
|
<DE.Block :col-count="3" :cell-flex="false">
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Indikasi Rawat Jalan" placeholder="Indikasi Rawat Jalan" :errors="errors" />
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Keluhan Utama (Singkat & Menunjang)" placeholder="Keluhan Utama (Singkat & Menunjang)" :errors="errors" />
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Pemeriksaan Fisik (Singkat & Menunjang)" placeholder="Pemeriksaan Fisik (Singkat & Menunjang)" :errors="errors" />
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Riwayat Penyakit" placeholder="Riwayat Penyakit" :errors="errors" />
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Diagnosa Medis" placeholder="Diagnosa Medis" :errors="errors" />
|
||||||
|
</DE.Block>
|
||||||
|
<Separator class="my-4" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- DIAGNOSIS -->
|
||||||
|
<h1 class="mb-3 text-base font-medium">Diagnosa</h1>
|
||||||
|
<h1 class="mb-3 text-base font-medium">Diagnosa Utama</h1>
|
||||||
|
<DE.Block :col-count="2" class="" :cell-flex="false">
|
||||||
|
<InputBase
|
||||||
|
field-name="diagnosis"
|
||||||
|
label="Diagnosa" placeholder="Masukkan Diagnosa"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
<SelectIcd10
|
||||||
|
field-name="primaryDiagnosis"
|
||||||
|
label="ICD 10"
|
||||||
|
placeholder="ICD 10"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
<DE.Cell :col-span="2">
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Dasar Diagnosa" placeholder="Masukkan Dasar Diagnosa utama Pasien" :errors="errors" />
|
||||||
|
</DE.Cell>
|
||||||
|
|
||||||
|
|
||||||
|
<DE.Cell :col-span="2">
|
||||||
|
<h1 class="mb-3 text-base font-medium">Diagnosa Sekunder</h1>
|
||||||
|
<div class="w-full rounded-md border bg-gray-50/50 p-4 shadow-sm dark:bg-neutral-950">
|
||||||
|
|
||||||
|
<FieldArray v-slot="{ fields, push, remove }" name="secondaryDiagnosis">
|
||||||
|
<div v-for="(field, idx) in fields" :key="idx" class="flex items-center gap-3 mb-3">
|
||||||
|
<div class="w-full">
|
||||||
|
<h1 class="font-medium">Diagnosa {{ idx + 1 }}</h1>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<InputBase
|
||||||
|
:field-name="`secondaryDiagnosis[${idx}].diagnosis`"
|
||||||
|
label="Diagnosa" placeholder="Masukkan Diagnosa"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
<SelectIcd10
|
||||||
|
:field-name="`secondaryDiagnosis[${idx}].icd10`"
|
||||||
|
label="ICD 10"
|
||||||
|
placeholder="ICD 10"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
</div>
|
||||||
|
<TextAreaInput :field-name="`secondaryDiagnosis[${idx}].diagnosisBasis`"
|
||||||
|
label="Dasar Diagnosa" placeholder="Masukkan Dasar Diagnosa Pasien" :errors="errors" />
|
||||||
|
</div>
|
||||||
|
<Button v-if="idx !== 0" type="button" variant="destructive" size="sm" @click="remove(idx)">
|
||||||
|
<Icon name="i-lucide-trash-2" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="outline"
|
||||||
|
class="w-full rounded-md border border-primary bg-white px-4 py-2 text-primary hover:border-primary hover:bg-primary hover:text-white sm:w-auto sm:text-sm"
|
||||||
|
@click="push(DEFAULT_SECONDARY_DIAGNOSIS_VALUE)">
|
||||||
|
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle transition-colors" />
|
||||||
|
Tambah Diagnosis
|
||||||
|
</Button>
|
||||||
|
</FieldArray>
|
||||||
|
</div>
|
||||||
|
</DE.Cell>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</DE.Block>
|
||||||
|
<Separator class="my-4" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Tindakan OPERATIF/NON OPERATIF -->
|
||||||
|
<div class="mb-3 flex gap-3 items-center">
|
||||||
|
<h1 class="text-base font-medium">Tindakan Operatif/Non Operatif</h1>
|
||||||
|
<Button variant="ghost"
|
||||||
|
class="gap-1 items-center text-orange-400"
|
||||||
|
@click="isActionHistoryOpen = true">
|
||||||
|
<Icon name="i-lucide-history" class="h-4 w-4" /> Riwayat Tindakan</Button>
|
||||||
|
</div>
|
||||||
|
<h1 class="mb-3 font-normal">Tindakan Operatif/Non Operatif Utama</h1>
|
||||||
|
<DE.Block :col-count="2" class="" :cell-flex="false">
|
||||||
|
<InputBase
|
||||||
|
field-name="diagnosis"
|
||||||
|
label="Tindakan" placeholder="Masukkan Tindakan"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
<SelectIcd9
|
||||||
|
field-name="primaryDiagnosis"
|
||||||
|
label="ICD 9"
|
||||||
|
placeholder="ICD 9"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
<DE.Cell :col-span="2">
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Dasar Tindakan" placeholder="Masukkan Dasar Tindakan utama Pasien" :errors="errors" />
|
||||||
|
</DE.Cell>
|
||||||
|
|
||||||
|
<DE.Cell :col-span="2">
|
||||||
|
<h1 class="mb-3 text-base font-medium">Tindakan Lain</h1>
|
||||||
|
<div class="w-full rounded-md border bg-gray-50/50 p-4 shadow-sm dark:bg-neutral-950">
|
||||||
|
|
||||||
|
<FieldArray v-slot="{ fields, push, remove }" name="secondaryOperativeNonOperativeAct">
|
||||||
|
<div v-for="(field, idx) in fields" :key="idx" class="flex items-center gap-3 mb-3">
|
||||||
|
<div class="w-full">
|
||||||
|
<h1 class="font-medium">Tindakan {{ idx + 1 }}</h1>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<InputBase
|
||||||
|
:field-name="`secondaryOperativeNonOperativeAct[${idx}].action`"
|
||||||
|
label="Tindakan" placeholder="Masukkan Tindakan"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
<SelectIcd10
|
||||||
|
:field-name="`secondaryOperativeNonOperativeAct[${idx}].icd9`"
|
||||||
|
label="ICD 10"
|
||||||
|
placeholder="ICD 10"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
</div>
|
||||||
|
<TextAreaInput :field-name="`secondaryOperativeNonOperativeAct[${idx}].actionBasis`"
|
||||||
|
label="Dasar Tindakan" placeholder="Masukkan Dasar Tindakan Pasien" :errors="errors" />
|
||||||
|
</div>
|
||||||
|
<Button v-if="idx !== 0" type="button" variant="destructive" size="sm" @click="remove(idx)">
|
||||||
|
<Icon name="i-lucide-trash-2" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="outline"
|
||||||
|
class="w-full rounded-md border border-primary bg-white px-4 py-2 text-primary hover:border-primary hover:bg-primary hover:text-white sm:w-auto sm:text-sm"
|
||||||
|
@click="push(DEFAULT_SECONDARY_ACTION_VALUE)">
|
||||||
|
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle transition-colors" />
|
||||||
|
Tambah Tindakan
|
||||||
|
</Button>
|
||||||
|
</FieldArray>
|
||||||
|
</div>
|
||||||
|
</DE.Cell>
|
||||||
|
|
||||||
|
</DE.Block>
|
||||||
|
<TextAreaInput :field-name="`medicalAction`"
|
||||||
|
label="Tindakan Medis" placeholder="Masukkan Tindakan Medis" :errors="errors" />
|
||||||
|
<Separator class="my-4" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- KONSULTASI -->
|
||||||
|
<div class="mb-3 flex gap-3 items-center">
|
||||||
|
<h1 class="text-base font-medium">Konsultasi</h1>
|
||||||
|
<Button variant="ghost"
|
||||||
|
class="gap-1 items-center text-orange-400"
|
||||||
|
@click="isConsultationHistoryOpen = true">
|
||||||
|
<Icon name="i-lucide-history" class="h-4 w-4" /> Riwayat Konsultasi</Button>
|
||||||
|
</div>
|
||||||
|
<div class="w-full rounded-md border bg-gray-50/50 p-4 shadow-sm dark:bg-neutral-950">
|
||||||
|
<FieldArray v-slot="{ fields, push, remove }" name="consultation">
|
||||||
|
<div v-for="(field, idx) in fields" :key="idx" class="flex items-center gap-3 mb-3">
|
||||||
|
<div class="w-full">
|
||||||
|
<h1 class="font-medium mb-1">Konsultasi {{ idx + 1 }}</h1>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<TextAreaInput :field-name="`consultation[${idx}].consultation`"
|
||||||
|
label="Konsultasi" placeholder="Masukkan Konsultasi" :errors="errors" />
|
||||||
|
<TextAreaInput :field-name="`consultation[${idx}].consultationReply`"
|
||||||
|
label="Jawaban Konsultasi" placeholder="Masukkan Jawaban Konsultasi" :errors="errors" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button v-if="idx !== 0" type="button" variant="destructive" size="sm" @click="remove(idx)">
|
||||||
|
<Icon name="i-lucide-trash-2" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="outline"
|
||||||
|
class="w-full rounded-md border border-primary bg-white px-4 py-2 text-primary hover:border-primary hover:bg-primary hover:text-white sm:w-auto sm:text-sm"
|
||||||
|
@click="push(DEFAULT_CONSULTATION_VALUE)">
|
||||||
|
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle transition-colors" />
|
||||||
|
Tambah Konsultasi
|
||||||
|
</Button>
|
||||||
|
</FieldArray>
|
||||||
|
</div>
|
||||||
|
<Separator class="my-4" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- DATA PENUNJANG -->
|
||||||
|
<section>
|
||||||
|
<div class="mb-3 flex gap-3 items-center">
|
||||||
|
<h1 class="text-base font-medium">Data Penunjang</h1>
|
||||||
|
<Button variant="ghost"
|
||||||
|
class="gap-1 items-center text-orange-400"
|
||||||
|
@click="isSupportingHistoryOpen = true">
|
||||||
|
<Icon name="i-lucide-history" class="h-4 w-4" /> Riwayat Data Penunjang</Button>
|
||||||
|
</div>
|
||||||
|
<DE.Block class="" :cell-flex="false">
|
||||||
|
<TextAreaInput field-name="supplementCheckup" label="Pemeriksaan Penunjang" placeholder="Masukkan Pemeriksaan Penunjang" :errors="errors" />
|
||||||
|
</DE.Block>
|
||||||
|
</section>
|
||||||
|
<Separator class="my-4" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- DATA Farmasi -->
|
||||||
|
<section>
|
||||||
|
<div class="mb-3 flex gap-3 items-center">
|
||||||
|
<h1 class="text-base font-medium">Data Farmasi</h1>
|
||||||
|
<Button variant="ghost"
|
||||||
|
class="gap-1 items-center text-orange-400"
|
||||||
|
@click="isFarmacyHistoryOpen = true">
|
||||||
|
<Icon name="i-lucide-history" class="h-4 w-4" /> Riwayat Data Farmasi</Button>
|
||||||
|
</div>
|
||||||
|
<DE.Block class="items-end" :col-count="2" :cell-flex="false">
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Kelainan Khusus Alergi" placeholder="Masukkan Kelainan Khusus Alergi" :errors="errors" />
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Kelainan Lain" placeholder="Masukkan Kelainan Lain" :errors="errors" />
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Terapi yang Diberikan (Farmakologi dan Non Farmakologi) Selama Dirawat" placeholder="Masukkan Terapi yang Diberikan (Farmakologi dan Non Farmakologi) Selama Dirawat" :errors="errors" />
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Terapi yang Diberikan (Farmakologi dan Non Farmakologi) Waktu Pulang" placeholder="Masukkan Terapi yang Diberikan (Farmakologi dan Non Farmakologi) Waktu Pulang" :errors="errors" />
|
||||||
|
<TextAreaInput field-name="supplementCheckup"
|
||||||
|
label="Instruksi Tindak Lanjut/Anjuran dan Edukasi (Follow Up/Kontrol)" placeholder="Masukkan Instruksi Tindak Lanjut/Anjuran dan Edukasi (Follow Up/Kontrol)" :errors="errors" />
|
||||||
|
</DE.Block>
|
||||||
|
</section>
|
||||||
|
<Separator class="my-4" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Keadaan Waktu Keluar -->
|
||||||
|
<section>
|
||||||
|
<h1 class="mb-3 font-medium">Keadaan Waktu Keluar</h1>
|
||||||
|
<DE.Block :col-count="4" :cell-flex="false">
|
||||||
|
<InputBase
|
||||||
|
:field-name="`aaaaaaaaaaa`"
|
||||||
|
label="Tekanan Darah Sistol" placeholder="Masukkan Tekanan Darah Sistol"
|
||||||
|
right-label="mmHg" bottom-label="Contoh: 90"
|
||||||
|
:errors="errors" is-required numeric-only />
|
||||||
|
<InputBase
|
||||||
|
:field-name="`aaaaaaaaaaa`"
|
||||||
|
label="Tekanan Darah Diastol" placeholder="Masukkan Tekanan Darah Diastol"
|
||||||
|
right-label="mmHg" bottom-label="Contoh: 60"
|
||||||
|
:errors="errors" is-required numeric-only />
|
||||||
|
<InputBase
|
||||||
|
:field-name="`aaaaaaaaaaa`"
|
||||||
|
label="Pernafasan" placeholder="Masukkan Pernafasan"
|
||||||
|
right-label="kali / menit" bottom-label="Contoh: 20"
|
||||||
|
:errors="errors" is-required numeric-only />
|
||||||
|
<InputBase
|
||||||
|
:field-name="`aaaaaaaaaaa`"
|
||||||
|
label="Denyut Jantung" placeholder="Masukkan Denyut Jantung"
|
||||||
|
right-label="kali / menit" bottom-label="Contoh: 20"
|
||||||
|
:errors="errors" is-required numeric-only />
|
||||||
|
<InputBase
|
||||||
|
:field-name="`aaaaaaaaaaa`"
|
||||||
|
label="Suhu Tubuh" placeholder="Masukkan Suhu Tubuh"
|
||||||
|
right-label="'C" bottom-label="Contoh: 37"
|
||||||
|
:errors="errors" is-required numeric-only />
|
||||||
|
<SelectConciousLevel
|
||||||
|
:field-name="`aaaaaaaaaaa`"
|
||||||
|
label="Tingkat Kesadaran"
|
||||||
|
placeholder="Tingkat Kesadaran"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
<SelectPainScale
|
||||||
|
:field-name="`aaaaaaaaaaa`"
|
||||||
|
label="Skala Nyeri"
|
||||||
|
placeholder="Skala Nyeri"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
</DE.Block>
|
||||||
|
</section>
|
||||||
|
<Separator class="my-4" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Program Nasional -->
|
||||||
|
<section>
|
||||||
|
<div class="mb-3 flex gap-3 items-center">
|
||||||
|
<h1 class="text-base font-medium">Program Nasional</h1>
|
||||||
|
<Button variant="ghost"
|
||||||
|
class="gap-1 items-center text-orange-400"
|
||||||
|
@click="isNationalProgramServiceHistoryOpen = true">
|
||||||
|
<Icon name="i-lucide-history" class="h-4 w-4" /> Riwayat Program Nasional</Button>
|
||||||
|
</div>
|
||||||
|
<DE.Block :col-count="3" :cell-flex="false">
|
||||||
|
<SelectNationalProgramService
|
||||||
|
:field-name="`aaaaaaaaaaa`"
|
||||||
|
label="Layanan Program Nasional"
|
||||||
|
placeholder="Layanan Program Nasional"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
<SelectNationalProgramServiceStatus
|
||||||
|
:field-name="`aaaaaaaaaaa`"
|
||||||
|
label="Status Layanan Program Nasional"
|
||||||
|
placeholder="Status Layanan Program Nasional"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
</DE.Block>
|
||||||
|
</section>
|
||||||
|
<Separator class="my-4" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Penatalaksanaan -->
|
||||||
|
<section>
|
||||||
|
<h1 class="mb-3 font-medium">Penatalaksanaan</h1>
|
||||||
|
<DE.Block :col-count="3" :cell-flex="false">
|
||||||
|
<SelectArrangement
|
||||||
|
field-name="arrangement"
|
||||||
|
label="Lanjutan Penatalaksanaan"
|
||||||
|
placeholder="Pilih Arrangement"
|
||||||
|
:errors="errors" />
|
||||||
|
<SelectHospitalLeaveCondition
|
||||||
|
:field-name="`aaaaaaaaaaa`"
|
||||||
|
label="Kondisi Meninggalkan RS"
|
||||||
|
placeholder="Kondisi Meninggalkan RS"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
<SelectHospitalLeaveMethod
|
||||||
|
:field-name="`aaaaaaaaaaa`"
|
||||||
|
label="Cara Keluar RS"
|
||||||
|
placeholder="Cara Keluar RS"
|
||||||
|
:errors="errors" is-required />
|
||||||
|
</DE.Block>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<DE.Cell :col-span="3"
|
||||||
|
v-show="resumeArrangementType === `mrs`">
|
||||||
|
<TextAreaInput
|
||||||
|
field-name="inpatientIndication"
|
||||||
|
label="Indikasi Rawat Jalan"
|
||||||
|
placeholder="Indikasi Rawat Jalan"
|
||||||
|
:errors="errors" />
|
||||||
|
</DE.Cell>
|
||||||
|
|
||||||
|
<DE.Block :col-count="3" :cell-flex="false">
|
||||||
|
<SelectFaskes
|
||||||
|
v-show="resumeArrangementType === `rujukExternal` "
|
||||||
|
field-name="faskes"
|
||||||
|
label="Faskes"
|
||||||
|
placeholder="Pilih Faskes"
|
||||||
|
:errors="errors"
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
v-show="resumeArrangementType === `rujukExternal` "
|
||||||
|
field-name="clinic"
|
||||||
|
label="Klinik"
|
||||||
|
placeholder="Masukkan Klinik"
|
||||||
|
:errors="errors"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectDate
|
||||||
|
v-show="resumeArrangementType === `meninggal`"
|
||||||
|
field-name="deathDate"
|
||||||
|
label="Jam Tanggal Meninggal"
|
||||||
|
:errors="errors"
|
||||||
|
:is-with-time="true" />
|
||||||
|
|
||||||
|
<DE.Cell class="mt-2" :col-span="3" v-show="resumeArrangementType === `meninggal`">
|
||||||
|
<div class="w-2/3 rounded-md border bg-gray-50/50 p-4 shadow-sm dark:bg-neutral-950">
|
||||||
|
<h1 class="mb-3 font-medium">Sebab Meninggal</h1>
|
||||||
|
|
||||||
|
<FieldArray v-slot="{ fields, push, remove }" name="deathCause">
|
||||||
|
<div v-for="(field, idx) in fields" :key="idx" class="flex items-center gap-3 mb-3">
|
||||||
|
<SelectDeathCause
|
||||||
|
:field-name="`deathCause[${idx}]`"
|
||||||
|
:errors="errors"
|
||||||
|
/>
|
||||||
|
<Button v-if="idx !== 0" type="button" variant="destructive" size="sm" @click="remove(idx)">
|
||||||
|
<Icon name="i-lucide-trash-2" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button v-if="fields.length < 3" type="button" variant="outline"
|
||||||
|
class="w-full rounded-md border border-primary bg-white px-4 py-2 text-primary hover:border-primary hover:bg-primary hover:text-white sm:w-auto sm:text-sm"
|
||||||
|
@click="push(``)">
|
||||||
|
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle transition-colors" />
|
||||||
|
Tambah Sebab Meninggal
|
||||||
|
</Button>
|
||||||
|
</FieldArray>
|
||||||
|
</div>
|
||||||
|
</DE.Cell>
|
||||||
|
|
||||||
|
<DE.Cell
|
||||||
|
v-show="resumeArrangementType === `meninggal`"
|
||||||
|
:col-span="3">
|
||||||
|
<TextAreaInput
|
||||||
|
field-name="deathCauseDescription"
|
||||||
|
label="Keterangan Sebab Meninggal"
|
||||||
|
placeholder="Keterangan Sebab Meninggal"
|
||||||
|
:errors="errors" />
|
||||||
|
</DE.Cell>
|
||||||
|
|
||||||
|
</DE.Block>
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
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 './action-list.cfg'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
||||||
|
import type { DateRange } from 'radix-vue'
|
||||||
|
import { CalendarIcon } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
dateValue: DateRange
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const isModalOpen = inject(`isActionHistoryOpen`) as Ref<boolean>
|
||||||
|
|
||||||
|
const df = new DateFormatter('en-US', { dateStyle: 'medium',})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
'update:dateValue': [value: DateRange]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isModalOpen" title="" size="2xl">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button variant="outline" :class="cn('mb-1 w-[280px] justify-start text-left font-normal',
|
||||||
|
!props.dateValue && 'text-muted-foreground')">
|
||||||
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||||
|
<template v-if="props.dateValue.start">
|
||||||
|
<template v-if="props.dateValue.end">
|
||||||
|
{{ df.format(props.dateValue.start.toDate(getLocalTimeZone())) }} -
|
||||||
|
{{ df.format(props.dateValue.end.toDate(getLocalTimeZone())) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
{{ df.format(props.dateValue.start.toDate(getLocalTimeZone())) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else> Pick a date </template>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-auto p-0">
|
||||||
|
<RangeCalendar v-model="props.dateValue" initial-focus :number-of-months="2"
|
||||||
|
@update:model-value="(date) => emit('update:dateValue', date)" />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
:skeleton-size="props.paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="props.paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||||
|
import type { Patient } from '~/models/patient'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
import { educationCodes, genderCodes } from '~/lib/constants'
|
||||||
|
import { calculateAge } from '~/lib/utils'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dvvp.vue'))
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{width: 140}, {}, {}, {width: 140}, {width: 10},],
|
||||||
|
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'Tanggal/Jam' },
|
||||||
|
{ label: 'Dokter' },
|
||||||
|
{ label: 'Tempat Layanan' },
|
||||||
|
{ label: 'Jenis' },
|
||||||
|
{ label: 'Jenis Pemeriksaan' },
|
||||||
|
{ label: 'Tanggal/Jam' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keys: ['birth_date', 'person.name', 'person.name', 'person.name', 'person.name', 'birth_date',],
|
||||||
|
|
||||||
|
delKeyNames: [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
],
|
||||||
|
|
||||||
|
parses: {
|
||||||
|
patientId: (rec: unknown): unknown => {
|
||||||
|
const patient = rec as Patient
|
||||||
|
return patient.number
|
||||||
|
},
|
||||||
|
identity_number: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (person.nationality == 'WNA') {
|
||||||
|
return person.passportNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
return person.residentIdentityNumber || '-'
|
||||||
|
},
|
||||||
|
birth_date: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.birthDate == 'object' && person.birthDate) {
|
||||||
|
return (person.birthDate as Date).toLocaleDateString('id-ID')
|
||||||
|
} else if (typeof person.birthDate == 'string') {
|
||||||
|
return (person.birthDate as string).substring(0, 10)
|
||||||
|
}
|
||||||
|
return person.birthDate
|
||||||
|
},
|
||||||
|
patient_age: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
return calculateAge(person.birthDate)
|
||||||
|
},
|
||||||
|
gender: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.gender_code == 'number' && person.gender_code >= 0) {
|
||||||
|
return person.gender_code
|
||||||
|
} else if (typeof person.gender_code === 'string' && person.gender_code) {
|
||||||
|
return genderCodes[person.gender_code] || '-'
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
education: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
if (typeof person.education_code == 'number' && person.education_code >= 0) {
|
||||||
|
return person.education_code
|
||||||
|
} else if (typeof person.education_code === 'string' && person.education_code) {
|
||||||
|
return educationCodes[person.education_code] || '-'
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
return {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
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 './consultation-list.cfg'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
||||||
|
import type { DateRange } from 'radix-vue'
|
||||||
|
import { CalendarIcon } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
dateValue: DateRange
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const isModalOpen = inject(`isConsultationHistoryOpen`) as Ref<boolean>
|
||||||
|
|
||||||
|
const df = new DateFormatter('en-US', { dateStyle: 'medium',})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
'update:dateValue': [value: DateRange]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isModalOpen" title="" size="2xl">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button variant="outline" :class="cn('mb-1 w-[280px] justify-start text-left font-normal',
|
||||||
|
!props.dateValue && 'text-muted-foreground')">
|
||||||
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||||
|
<template v-if="props.dateValue.start">
|
||||||
|
<template v-if="props.dateValue.end">
|
||||||
|
{{ df.format(props.dateValue.start.toDate(getLocalTimeZone())) }} -
|
||||||
|
{{ df.format(props.dateValue.end.toDate(getLocalTimeZone())) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
{{ df.format(props.dateValue.start.toDate(getLocalTimeZone())) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else> Pick a date </template>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-auto p-0">
|
||||||
|
<RangeCalendar v-model="props.dateValue" initial-focus :number-of-months="2"
|
||||||
|
@update:model-value="(date) => emit('update:dateValue', date)" />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
:skeleton-size="props.paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="props.paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||||
|
import type { Patient } from '~/models/patient'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
const lampiranBtn = defineAsyncComponent(() => import('../_common/print-btn.vue'))
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{}, {}, {}, {}, {},],
|
||||||
|
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'Tanggal/Jam' },
|
||||||
|
{ label: 'Dokter' },
|
||||||
|
{ label: 'Tempat Layanan' },
|
||||||
|
{ label: 'KSM' },
|
||||||
|
{ label: 'Tanggal/Jam' },
|
||||||
|
{ label: 'Tujuan' },
|
||||||
|
{ label: 'Dokter' },
|
||||||
|
{ label: 'Berkas' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keys: ['birth_date', 'person.name', 'person.name', 'person.name', 'person.name', 'birth_date','person.name', 'action', ],
|
||||||
|
|
||||||
|
parses: {
|
||||||
|
birth_date: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.birthDate == 'object' && person.birthDate) {
|
||||||
|
return (person.birthDate as Date).toLocaleDateString('id-ID')
|
||||||
|
} else if (typeof person.birthDate == 'string') {
|
||||||
|
return (person.birthDate as string).substring(0, 10)
|
||||||
|
}
|
||||||
|
return person.birthDate
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
return {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: lampiranBtn,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
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 './farmacy-list.cfg'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
||||||
|
import type { DateRange } from 'radix-vue'
|
||||||
|
import { CalendarIcon } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
dateValue: DateRange
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const isModalOpen = inject(`isFarmacyHistoryOpen`) as Ref<boolean>
|
||||||
|
|
||||||
|
const df = new DateFormatter('en-US', { dateStyle: 'medium',})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
'update:dateValue': [value: DateRange]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isModalOpen" title="" size="2xl">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button variant="outline" :class="cn('mb-1 w-[280px] justify-start text-left font-normal',
|
||||||
|
!props.dateValue && 'text-muted-foreground')">
|
||||||
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||||
|
<template v-if="props.dateValue.start">
|
||||||
|
<template v-if="props.dateValue.end">
|
||||||
|
{{ df.format(props.dateValue.start.toDate(getLocalTimeZone())) }} -
|
||||||
|
{{ df.format(props.dateValue.end.toDate(getLocalTimeZone())) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
{{ df.format(props.dateValue.start.toDate(getLocalTimeZone())) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else> Pick a date </template>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-auto p-0">
|
||||||
|
<RangeCalendar v-model="props.dateValue" initial-focus :number-of-months="2"
|
||||||
|
@update:model-value="(date) => emit('update:dateValue', date)" />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
:skeleton-size="props.paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="props.paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||||
|
import type { Patient } from '~/models/patient'
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{}, {}, {}, {}, {},],
|
||||||
|
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'Tanggal Order' },
|
||||||
|
{ label: 'No Resep' },
|
||||||
|
{ label: 'Tempat Layanan' },
|
||||||
|
{ label: 'Nama Obat' },
|
||||||
|
{ label: 'Tanggal Disetujui' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keys: ['birth_date', 'person.name', 'person.name', 'person.name', 'birth_date',],
|
||||||
|
|
||||||
|
parses: {
|
||||||
|
birth_date: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.birthDate == 'object' && person.birthDate) {
|
||||||
|
return (person.birthDate as Date).toLocaleDateString('id-ID')
|
||||||
|
} else if (typeof person.birthDate == 'string') {
|
||||||
|
return (person.birthDate as string).substring(0, 10)
|
||||||
|
}
|
||||||
|
return person.birthDate
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
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 './national-program-list.cfg'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
||||||
|
import type { DateRange } from 'radix-vue'
|
||||||
|
import { CalendarIcon } from 'lucide-vue-next'
|
||||||
|
import type { Item } from '~/components/pub/my-ui/combobox'
|
||||||
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
searchValue: string
|
||||||
|
statusValue: string
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const isModalOpen = inject(`isNationalProgramServiceHistoryOpen`) as Ref<boolean>
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
'update:searchValue': [value: string]
|
||||||
|
'update:statusValue': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusOptions: Item[] = [
|
||||||
|
{ value: 'all', label: 'All Statuses' },
|
||||||
|
{ value: 'pending', label: 'Pending' },
|
||||||
|
{ value: 'active', label: 'Active' },
|
||||||
|
{ value: 'completed', label: 'Completed' },
|
||||||
|
{ value: 'archived', label: 'Archived' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isModalOpen" title="" size="2xl">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<DE.Block :col-count="4" class="" :cell-flex="false">
|
||||||
|
<Input
|
||||||
|
v-model="props.searchValue"
|
||||||
|
class=""
|
||||||
|
placeholder="Cari .."/>
|
||||||
|
<Select
|
||||||
|
:items="statusOptions"
|
||||||
|
:model-value="props.statusValue"
|
||||||
|
@update:model-value="(data) => emit('update:statusValue', data)"
|
||||||
|
/>
|
||||||
|
</DE.Block>
|
||||||
|
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
:skeleton-size="props.paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="props.paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||||
|
import type { Patient } from '~/models/patient'
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{}, {}, {}, {}, {},],
|
||||||
|
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'Nomor' },
|
||||||
|
{ label: 'Layanan Program Nasional' },
|
||||||
|
{ label: 'Status' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keys: ['person.name', 'person.name', 'person.name',],
|
||||||
|
|
||||||
|
parses: {
|
||||||
|
// birth_date: (rec: unknown): unknown => {
|
||||||
|
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
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 './supporting-list.cfg'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
||||||
|
import type { DateRange } from 'radix-vue'
|
||||||
|
import { CalendarIcon } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
dateValue: DateRange
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const isModalOpen = inject(`isSupportingHistoryOpen`) as Ref<boolean>
|
||||||
|
|
||||||
|
const df = new DateFormatter('en-US', { dateStyle: 'medium',})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
'update:dateValue': [value: DateRange]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isModalOpen" title="" size="2xl">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button variant="outline" :class="cn('mb-1 w-[280px] justify-start text-left font-normal',
|
||||||
|
!props.dateValue && 'text-muted-foreground')">
|
||||||
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||||
|
<template v-if="props.dateValue.start">
|
||||||
|
<template v-if="props.dateValue.end">
|
||||||
|
{{ df.format(props.dateValue.start.toDate(getLocalTimeZone())) }} -
|
||||||
|
{{ df.format(props.dateValue.end.toDate(getLocalTimeZone())) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
{{ df.format(props.dateValue.start.toDate(getLocalTimeZone())) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else> Pick a date </template>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-auto p-0">
|
||||||
|
<RangeCalendar v-model="props.dateValue" initial-focus :number-of-months="2"
|
||||||
|
@update:model-value="(date) => emit('update:dateValue', date)" />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
:skeleton-size="props.paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="props.paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||||
|
import type { Patient } from '~/models/patient'
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{}, {}, {}, {}, {},],
|
||||||
|
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'Tanggal Order' },
|
||||||
|
{ label: 'No Lab' },
|
||||||
|
{ label: 'Nama Pemeriksaan' },
|
||||||
|
{ label: 'Tanggal Pemeriksaan' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keys: ['birth_date', 'person.name', 'person.name', 'birth_date',],
|
||||||
|
|
||||||
|
parses: {
|
||||||
|
birth_date: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.birthDate == 'object' && person.birthDate) {
|
||||||
|
return (person.birthDate as Date).toLocaleDateString('id-ID')
|
||||||
|
} else if (typeof person.birthDate == 'string') {
|
||||||
|
return (person.birthDate as string).substring(0, 10)
|
||||||
|
}
|
||||||
|
return person.birthDate
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {
|
||||||
|
// patient_address(_rec) {
|
||||||
|
// return '-'
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||||
|
import type { Patient } from '~/models/patient'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
import { educationCodes, genderCodes } from '~/lib/constants'
|
||||||
|
import { calculateAge } from '~/lib/utils'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dvvp.vue'))
|
||||||
|
const statusBadge = defineAsyncComponent(() => import('./_common/verify-badge.vue'))
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{width: 140}, {}, {}, {width: 140}, {width: 10},],
|
||||||
|
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'Tgl Simpan' },
|
||||||
|
{ label: 'DPJP' },
|
||||||
|
{ label: 'KSM' },
|
||||||
|
{ label: 'Status' },
|
||||||
|
{ label: 'Action' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keys: ['birth_date', 'number', 'person.name', 'status', 'action'],
|
||||||
|
|
||||||
|
delKeyNames: [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
],
|
||||||
|
|
||||||
|
parses: {
|
||||||
|
patientId: (rec: unknown): unknown => {
|
||||||
|
const patient = rec as Patient
|
||||||
|
return patient.number
|
||||||
|
},
|
||||||
|
identity_number: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (person.nationality == 'WNA') {
|
||||||
|
return person.passportNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
return person.residentIdentityNumber || '-'
|
||||||
|
},
|
||||||
|
birth_date: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.birthDate == 'object' && person.birthDate) {
|
||||||
|
return (person.birthDate as Date).toLocaleDateString('id-ID')
|
||||||
|
} else if (typeof person.birthDate == 'string') {
|
||||||
|
return (person.birthDate as string).substring(0, 10)
|
||||||
|
}
|
||||||
|
return person.birthDate
|
||||||
|
},
|
||||||
|
patient_age: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
return calculateAge(person.birthDate)
|
||||||
|
},
|
||||||
|
gender: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.gender_code == 'number' && person.gender_code >= 0) {
|
||||||
|
return person.gender_code
|
||||||
|
} else if (typeof person.gender_code === 'string' && person.gender_code) {
|
||||||
|
return genderCodes[person.gender_code] || '-'
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
education: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
if (typeof person.education_code == 'number' && person.education_code >= 0) {
|
||||||
|
return person.education_code
|
||||||
|
} else if (typeof person.education_code === 'string' && person.education_code) {
|
||||||
|
return educationCodes[person.education_code] || '-'
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
return {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
status(rec, idx) {
|
||||||
|
return {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: statusBadge,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||||
|
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||||
|
import { config } from './list.cfg'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
:skeleton-size="paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||||
|
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||||
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
|
import { Form } from '~/components/pub/ui/form'
|
||||||
|
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||||
|
import type { InstallationFormData } from '~/schemas/installation.schema'
|
||||||
|
import TextCaptcha from '~/components/pub/my-ui/form/text-captcha.vue'
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
schema: any
|
||||||
|
errors?: FormErrors
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [values: InstallationFormData, resetForm: () => void]
|
||||||
|
cancel: [resetForm: () => void]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formSchema = toTypedSchema(props.schema)
|
||||||
|
const captchaRef = ref<InstanceType<typeof TextCaptcha> | null>(null)
|
||||||
|
|
||||||
|
// Form submission handler
|
||||||
|
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
|
||||||
|
const formData: InstallationFormData = {
|
||||||
|
name: values.name || '',
|
||||||
|
code: values.code || '',
|
||||||
|
}
|
||||||
|
emit('submit', formData, resetForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form cancel handler
|
||||||
|
function onCancelForm({ resetForm }: { resetForm: () => void }) {
|
||||||
|
emit('cancel', resetForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = ref([
|
||||||
|
{ label: 'Rujukan Internal', value: 'ri' },
|
||||||
|
{ label: 'SEP Rujukan', value: 'sr' },
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
v-slot="{ handleSubmit, resetForm }"
|
||||||
|
as=""
|
||||||
|
keep-values
|
||||||
|
:validation-schema="formSchema"
|
||||||
|
>
|
||||||
|
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
|
||||||
|
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||||
|
<div class="flex flex-col justify-between">
|
||||||
|
|
||||||
|
<InputBase
|
||||||
|
field-name="name"
|
||||||
|
label="Nama"
|
||||||
|
placeholder="Masukkan Nama"
|
||||||
|
:errors="errors"/>
|
||||||
|
<InputBase
|
||||||
|
field-name="email"
|
||||||
|
label="Email"
|
||||||
|
placeholder="Masukkan Email"
|
||||||
|
:errors="errors"/>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<Label class="" for="password">Password</Label>
|
||||||
|
<Field class="" id="password" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="password">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
v-bind="componentField"
|
||||||
|
type="password"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextCaptcha
|
||||||
|
ref="captchaRef"
|
||||||
|
:length="5"
|
||||||
|
:useSpacing="true"
|
||||||
|
:noiseChars="true"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { ExposedForm } from '~/types/form'
|
||||||
|
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||||
|
import { handleActionSave,} from '~/handlers/supporting-document.handler'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||||
|
import { DocumentUploadSchema } from '~/schemas/document-upload.schema'
|
||||||
|
import { toFormData } from '~/lib/utils'
|
||||||
|
import { uploadAttachment } from '~/services/supporting-document.service'
|
||||||
|
|
||||||
|
// #region Props & Emits
|
||||||
|
const props = defineProps<{
|
||||||
|
callbackUrl?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// form related state
|
||||||
|
const route = useRoute()
|
||||||
|
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||||
|
const inputForm = ref<ExposedForm<any> | null>(null)
|
||||||
|
const { user } = useUserStore()
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region State & Computed
|
||||||
|
const router = useRouter()
|
||||||
|
const isConfirmationOpen = ref(false)
|
||||||
|
const initialValues = {
|
||||||
|
officer: user.user_name,
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle Hooks
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
function goBack() {
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function handleConfirmAdd() {
|
||||||
|
const inputData = await composeFormData()
|
||||||
|
const inputFormData: FormData = toFormData(inputData)
|
||||||
|
|
||||||
|
const response = await handleActionSave(inputFormData, () => { }, () => { }, toast, )
|
||||||
|
const data = (response?.body?.data ?? null)
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
|
// // If has callback provided redirect to callback with patientData
|
||||||
|
if (props.callbackUrl) {
|
||||||
|
navigateTo(props.callbackUrl + '?control-letter-id=' + inputData.id)
|
||||||
|
}
|
||||||
|
goBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function composeFormData(): Promise<any> {
|
||||||
|
inputForm.value?.setValues({
|
||||||
|
...inputForm.value?.values,
|
||||||
|
ref_id: encounterId,
|
||||||
|
upload_employee_id: user.employee_id
|
||||||
|
})
|
||||||
|
|
||||||
|
const [inputFormState,] = await Promise.all([
|
||||||
|
inputForm.value?.validate(),
|
||||||
|
])
|
||||||
|
|
||||||
|
const results = [inputFormState]
|
||||||
|
const allValid = results.every((r) => r?.valid)
|
||||||
|
|
||||||
|
// exit, if form errors happend during validation
|
||||||
|
if (!allValid) {
|
||||||
|
toast({ title: 'Form validation failed', variant: 'destructive',})
|
||||||
|
return Promise.reject('Form validation failed')
|
||||||
|
}
|
||||||
|
const formData = inputFormState?.values
|
||||||
|
return new Promise((resolve) => resolve(formData))
|
||||||
|
}
|
||||||
|
// #endregion region
|
||||||
|
|
||||||
|
// #region Utilities & event handlers
|
||||||
|
async function handleActionClick(eventType: string) {
|
||||||
|
if (eventType === 'submit') {
|
||||||
|
isConfirmationOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'back') {
|
||||||
|
if (props.callbackUrl) {
|
||||||
|
await navigateTo(props.callbackUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
goBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelAdd() {
|
||||||
|
isConfirmationOpen.value = false
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">
|
||||||
|
<h1>Upload Dokumen</h1>
|
||||||
|
</div>
|
||||||
|
<AppDocumentUploadEntryForm
|
||||||
|
ref="inputForm"
|
||||||
|
:schema="DocumentUploadSchema"
|
||||||
|
:initial-values="initialValues"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mb-2 flex justify-end py-2">
|
||||||
|
<Action :enable-draft="false" @click="handleActionClick" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Confirmation v-model:open="isConfirmationOpen"
|
||||||
|
title="Simpan Data"
|
||||||
|
message="Apakah Anda yakin ingin menyimpan data ini?"
|
||||||
|
confirm-text="Simpan"
|
||||||
|
@confirm="handleConfirmAdd"
|
||||||
|
@cancel="handleCancelAdd" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* component style */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { ExposedForm } from '~/types/form'
|
||||||
|
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||||
|
import { handleActionSave,} from '~/handlers/supporting-document.handler'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||||
|
import { DocumentUploadSchema } from '~/schemas/document-upload.schema'
|
||||||
|
import { getDetail } from '~/services/supporting-document.service'
|
||||||
|
|
||||||
|
// #region Props & Emits
|
||||||
|
const props = defineProps<{
|
||||||
|
callbackUrl?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// form related state
|
||||||
|
const route = useRoute()
|
||||||
|
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||||
|
const docId = typeof route.params.document_id == 'string' ? parseInt(route.params.document_id) : 0
|
||||||
|
const inputForm = ref<ExposedForm<any> | null>(null)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region State & Computed
|
||||||
|
const router = useRouter()
|
||||||
|
const isConfirmationOpen = ref(false)
|
||||||
|
const { user } = useUserStore()
|
||||||
|
const initialValues = {
|
||||||
|
officer: user.user_name,
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle Hooks
|
||||||
|
onMounted(async () => {
|
||||||
|
const result = await getDetail(docId)
|
||||||
|
if (result.success) {
|
||||||
|
inputForm.value?.setValues(result.body.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
function goBack() {
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmAdd() {
|
||||||
|
const inputData = await composeFormData()
|
||||||
|
let createdDataId = 0
|
||||||
|
|
||||||
|
// const response = await handleActionSave(
|
||||||
|
// inputData,
|
||||||
|
// () => { },
|
||||||
|
// () => { },
|
||||||
|
// toast,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const data = (response?.body?.data ?? null)
|
||||||
|
// if (!data) return
|
||||||
|
// createdDataId = data.id
|
||||||
|
|
||||||
|
// // // If has callback provided redirect to callback with patientData
|
||||||
|
// if (props.callbackUrl) {
|
||||||
|
// navigateTo(props.callbackUrl + '?control-letter-id=' + inputData.id)
|
||||||
|
// }
|
||||||
|
// goBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function composeFormData(): Promise<any> {
|
||||||
|
const [inputFormState,] = await Promise.all([
|
||||||
|
inputForm.value?.validate(),
|
||||||
|
])
|
||||||
|
|
||||||
|
const results = [inputFormState]
|
||||||
|
const allValid = results.every((r) => r?.valid)
|
||||||
|
|
||||||
|
// exit, if form errors happend during validation
|
||||||
|
if (!allValid) return Promise.reject('Form validation failed')
|
||||||
|
|
||||||
|
const formData = inputFormState?.values
|
||||||
|
formData.encounter_id = encounterId
|
||||||
|
return new Promise((resolve) => resolve(formData))
|
||||||
|
}
|
||||||
|
// #endregion region
|
||||||
|
|
||||||
|
// #region Utilities & event handlers
|
||||||
|
async function handleActionClick(eventType: string) {
|
||||||
|
if (eventType === 'submit') {
|
||||||
|
isConfirmationOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'back') {
|
||||||
|
if (props.callbackUrl) {
|
||||||
|
await navigateTo(props.callbackUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelAdd() {
|
||||||
|
isConfirmationOpen.value = false
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">
|
||||||
|
<h1>Upload Dokumen</h1>
|
||||||
|
</div>
|
||||||
|
<AppDocumentUploadEntryForm
|
||||||
|
ref="inputForm"
|
||||||
|
:schema="DocumentUploadSchema"
|
||||||
|
:initial-values="initialValues"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="my-2 flex justify-end py-2">
|
||||||
|
<Action :enable-draft="false" @click="handleActionClick" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Confirmation v-model:open="isConfirmationOpen"
|
||||||
|
title="Simpan Data"
|
||||||
|
message="Apakah Anda yakin ingin menyimpan data ini?"
|
||||||
|
confirm-text="Simpan"
|
||||||
|
@confirm="handleConfirmAdd"
|
||||||
|
@cancel="handleCancelAdd" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* component style */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// #region Imports
|
||||||
|
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||||
|
import type { HeaderPrep, RefSearchNav, } from '~/components/pub/my-ui/data/types'
|
||||||
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
import { getList, remove } from '~/services/supporting-document.service'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
import type { Encounter } from '~/models/encounter'
|
||||||
|
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||||
|
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 { unauthorizedToast } from '~/lib/utils'
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
|
||||||
|
// #region Permission
|
||||||
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/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 { data, paginationMeta, handlePageChange, handleSearch, searchInput, fetchData } = usePaginatedList({
|
||||||
|
fetchFn: (params) => getList({
|
||||||
|
'encounter-id': encounterId,
|
||||||
|
// includes: "employee",
|
||||||
|
...params,
|
||||||
|
}),
|
||||||
|
entityName: 'encounter-document',
|
||||||
|
})
|
||||||
|
|
||||||
|
const isDocPreviewDialogOpen = ref(false)
|
||||||
|
const isRecordConfirmationOpen = ref(false)
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
const timestamp = ref<number>(0)
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: "Upload Dokumen",
|
||||||
|
icon: 'i-lucide-newspaper',
|
||||||
|
}
|
||||||
|
if (pagePermission.canCreate) {
|
||||||
|
headerPrep.addNav = {
|
||||||
|
label: "Upload Dokumen",
|
||||||
|
onClick: () => navigateTo({
|
||||||
|
name: 'rehab-encounter-id-document-upload-add',
|
||||||
|
params: { id: encounterId },
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refSearchNav: RefSearchNav = {
|
||||||
|
onClick: () => {
|
||||||
|
// open filter modal
|
||||||
|
},
|
||||||
|
onInput: (val: string) => {
|
||||||
|
searchInput.value = val
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
searchInput.value = ''
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle Hooks
|
||||||
|
onMounted(() => {
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
|
||||||
|
async function handleConfirmDelete(record: any, action: string) {
|
||||||
|
if (action === 'delete' && record?.id) {
|
||||||
|
try {
|
||||||
|
const result = await remove(record.id)
|
||||||
|
if (result.success) {
|
||||||
|
toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
|
||||||
|
fetchData()
|
||||||
|
} else {
|
||||||
|
toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleCancelConfirmation() {
|
||||||
|
// Reset record state when cancelled
|
||||||
|
recId.value = 0
|
||||||
|
recAction.value = ''
|
||||||
|
recItem.value = null
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Provide
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('timestamp', timestamp)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
watch([recId, recAction, timestamp], () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showDetail:
|
||||||
|
isDocPreviewDialogOpen.value = true
|
||||||
|
break
|
||||||
|
case ActionEvents.showEdit:
|
||||||
|
if(pagePermission.canUpdate){
|
||||||
|
navigateTo({
|
||||||
|
name: 'rehab-encounter-id-document-upload-document_id-edit',
|
||||||
|
params: { id: encounterId, "document_id": recId.value },
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
unauthorizedToast()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
if(pagePermission.canDelete){
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
} else {
|
||||||
|
unauthorizedToast()
|
||||||
|
}
|
||||||
|
navigateTo(recItem.value.filePath, { external: true, open: { target: '_blank' } })
|
||||||
|
break
|
||||||
|
case ActionEvents.showEdit:
|
||||||
|
navigateTo({
|
||||||
|
name: 'rehab-encounter-id-document-upload-document_id-edit',
|
||||||
|
params: { id: encounterId, "document_id": recId.value },
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header :prep="{ ...headerPrep }"
|
||||||
|
v-model:search="searchInput"
|
||||||
|
:ref-search-nav="refSearchNav"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
<AppDocumentUploadList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange"/>
|
||||||
|
|
||||||
|
<RecordConfirmation v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
|
||||||
|
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation">
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>ID:</strong>
|
||||||
|
{{ record?.id }}
|
||||||
|
</p>
|
||||||
|
<p v-if="record?.name">
|
||||||
|
<strong>Nama:</strong>
|
||||||
|
{{ record?.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
|
||||||
|
<Dialog v-model:open="isDocPreviewDialogOpen" title="Preview Dokumen" size="2xl">
|
||||||
|
<DocPreviewDialog :link="recItem?.filePath" />
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -9,6 +9,8 @@ import { getDetail } from '~/services/encounter.service'
|
|||||||
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
|
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
|
||||||
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
|
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
|
||||||
|
|
||||||
|
import { genEncounter } from '~/models/encounter'
|
||||||
|
|
||||||
// PLASE ORDER BY TAB POSITION
|
// PLASE ORDER BY TAB POSITION
|
||||||
import Status from '~/components/content/encounter/status.vue'
|
import Status from '~/components/content/encounter/status.vue'
|
||||||
import AssesmentFunctionList from '~/components/content/soapi/entry.vue'
|
import AssesmentFunctionList from '~/components/content/soapi/entry.vue'
|
||||||
@@ -19,6 +21,9 @@ import Prescription from '~/components/content/prescription/main.vue'
|
|||||||
import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
|
import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
|
||||||
import Radiology from '~/components/content/radiology-order/main.vue'
|
import Radiology from '~/components/content/radiology-order/main.vue'
|
||||||
import Consultation from '~/components/content/consultation/list.vue'
|
import Consultation from '~/components/content/consultation/list.vue'
|
||||||
|
import DocUploadList from '~/components/content/document-upload/list.vue'
|
||||||
|
import GeneralConsentList from '~/components/content/general-consent/entry.vue'
|
||||||
|
import ResumeList from '~/components/content/resume/list.vue'
|
||||||
import ControlLetterList from '~/components/content/control-letter/list.vue'
|
import ControlLetterList from '~/components/content/control-letter/list.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -33,12 +38,18 @@ const activeTab = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||||
const dataRes = await getDetail(id, {
|
const data = ref(genEncounter())
|
||||||
includes:
|
|
||||||
'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person',
|
async function fetchDetail() {
|
||||||
|
const res = await getDetail(id, {
|
||||||
|
includes: 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person,EncounterDocuments',
|
||||||
|
})
|
||||||
|
if(res.body?.data) data.value = res.body?.data
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDetail()
|
||||||
})
|
})
|
||||||
const dataResBody = dataRes.body ?? null
|
|
||||||
const data = dataResBody?.data ?? null
|
|
||||||
|
|
||||||
const tabs: TabItem[] = [
|
const tabs: TabItem[] = [
|
||||||
{ value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } },
|
{ value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } },
|
||||||
@@ -62,21 +73,23 @@ const tabs: TabItem[] = [
|
|||||||
},
|
},
|
||||||
{ value: 'therapy-protocol', label: 'Protokol Terapi' },
|
{ value: 'therapy-protocol', label: 'Protokol Terapi' },
|
||||||
{ value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' },
|
{ value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' },
|
||||||
{ value: 'consent', label: 'General Consent' },
|
{ value: 'consent', label: 'General Consent', component: GeneralConsentList, props: { encounter: data } },
|
||||||
{ value: 'patient-note', label: 'CPRJ' },
|
{ value: 'patient-note', label: 'CPRJ' },
|
||||||
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.id } },
|
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.value.id } },
|
||||||
{ value: 'device-order', label: 'Order Alkes', component: DeviceOrder, props: { encounter_id: data.id } },
|
{ value: 'device-order', label: 'Order Alkes', component: DeviceOrder, props: { encounter_id: data.value.id } },
|
||||||
{ value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.id } },
|
{ value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.value.id } },
|
||||||
{ value: 'mcu-lab-cp', label: 'Order Lab PK', component: CpLabOrder, props: { encounter_id: data.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-micro', label: 'Order Lab Mikro' },
|
||||||
{ value: 'mcu-lab-pa', label: 'Order Lab PA' },
|
{ value: 'mcu-lab-pa', label: 'Order Lab PA' },
|
||||||
{ value: 'medical-action', label: 'Order Ruang Tindakan' },
|
{ value: 'medical-action', label: 'Order Ruang Tindakan' },
|
||||||
{ value: 'mcu-result', label: 'Hasil Penunjang' },
|
{ value: 'mcu-result', label: 'Hasil Penunjang' },
|
||||||
{ value: 'consultation', label: 'Konsultasi', component: Consultation, props: { encounter: data } },
|
{ value: 'consultation', label: 'Konsultasi', component: Consultation, props: { encounter: data } },
|
||||||
|
{ value: 'resume', label: 'Resume', component: ResumeList, props: { encounter: data } },
|
||||||
|
{ value: 'control', label: 'Surat Kontrol' },
|
||||||
{ value: 'resume', label: 'Resume' },
|
{ value: 'resume', label: 'Resume' },
|
||||||
{ value: 'control', label: 'Surat Kontrol', component: ControlLetterList, props: { encounter: data } },
|
{ value: 'control', label: 'Surat Kontrol', component: ControlLetterList, props: { encounter: data } },
|
||||||
{ value: 'screening', label: 'Skrinning MPP' },
|
{ value: 'screening', label: 'Skrinning MPP' },
|
||||||
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung' },
|
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung', component: DocUploadList, props: { encounter: data }, },
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ watch([recId, recAction], () => {
|
|||||||
getCurrentMaterialDetail(recId.value)
|
getCurrentMaterialDetail(recId.value)
|
||||||
title.value = 'Edit Perlengkapan'
|
title.value = 'Edit Perlengkapan'
|
||||||
isReadonly.value = false
|
isReadonly.value = false
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
break
|
break
|
||||||
case ActionEvents.showConfirmDelete:
|
case ActionEvents.showConfirmDelete:
|
||||||
isRecordConfirmationOpen.value = true
|
isRecordConfirmationOpen.value = true
|
||||||
@@ -158,7 +159,7 @@ onMounted(async () => {
|
|||||||
@submit="
|
@submit="
|
||||||
(values: MaterialFormData, resetForm: any) => {
|
(values: MaterialFormData, resetForm: any) => {
|
||||||
if (recId > 0) {
|
if (recId > 0) {
|
||||||
handleActionEdit(recId, values, getEquipmentList, resetForm, toast)
|
handleActionEdit(recItem.code, values, getEquipmentList, resetForm, toast)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleActionSave(values, getEquipmentList, resetForm, toast)
|
handleActionSave(values, getEquipmentList, resetForm, toast)
|
||||||
@@ -173,7 +174,7 @@ onMounted(async () => {
|
|||||||
v-model:open="isRecordConfirmationOpen"
|
v-model:open="isRecordConfirmationOpen"
|
||||||
action="delete"
|
action="delete"
|
||||||
:record="recItem"
|
:record="recItem"
|
||||||
@confirm="() => handleActionRemove(recId, getEquipmentList, toast)"
|
@confirm="() => handleActionRemove(recItem.code, getEquipmentList, toast)"
|
||||||
@cancel=""
|
@cancel=""
|
||||||
>
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useQueryMode } from '@/composables/useQueryMode'
|
||||||
|
|
||||||
|
import List from './list.vue'
|
||||||
|
import Form from './form.vue'
|
||||||
|
|
||||||
|
// Models
|
||||||
|
import type { Encounter } from '~/models/encounter'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
interface Props {
|
||||||
|
encounter: Encounter
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const { mode, goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<List
|
||||||
|
v-if="mode === 'list'"
|
||||||
|
:encounter="props.encounter"
|
||||||
|
@add="goToEntry"
|
||||||
|
@edit="goToEntry"
|
||||||
|
/>
|
||||||
|
<Form
|
||||||
|
v-else
|
||||||
|
@back="backToList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { z } from 'zod'
|
||||||
|
import Entry from '~/components/app/general-consent/entry.vue'
|
||||||
|
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su-pr.vue'
|
||||||
|
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import { GeneralConsentSchema } from '~/schemas/general-consent.schema'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
import { handleActionSave, handleActionEdit } from '~/handlers/general-consent.handler'
|
||||||
|
import { create } from '~/services/generate-file.service'
|
||||||
|
// Services
|
||||||
|
import { getDetail } from '~/services/general-consent.service'
|
||||||
|
const { backToList } = useQueryCRUDMode('mode')
|
||||||
|
const { recordId } = useQueryCRUDRecordId('record-id')
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const isOpenProcedure = ref(false)
|
||||||
|
const isOpenDiagnose = ref(false)
|
||||||
|
const isOpenFungsional = ref(false)
|
||||||
|
const procedures = ref([])
|
||||||
|
const diagnoses = ref([])
|
||||||
|
const fungsional = ref([])
|
||||||
|
const selectedProcedure = ref<any>(null)
|
||||||
|
const selectedDiagnose = ref<any>(null)
|
||||||
|
const selectedFungsional = ref<any>(null)
|
||||||
|
const schema = GeneralConsentSchema
|
||||||
|
const payload = ref({
|
||||||
|
encounter_id: 0,
|
||||||
|
value: '',
|
||||||
|
})
|
||||||
|
const model = ref({
|
||||||
|
relatives: [],
|
||||||
|
responsibleName: '',
|
||||||
|
responsiblePhone: '',
|
||||||
|
informant: '',
|
||||||
|
witness1: '',
|
||||||
|
witness2: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileUrl = ref('')
|
||||||
|
const isLoading = reactive<DataTableLoader>({
|
||||||
|
isTableLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function getDiagnoses() {
|
||||||
|
isLoading.isTableLoading = true
|
||||||
|
const resp = await xfetch('/api/v1/diagnose-src')
|
||||||
|
if (resp.success) {
|
||||||
|
diagnoses.value = (resp.body as Record<string, any>).data
|
||||||
|
}
|
||||||
|
isLoading.isTableLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProcedures() {
|
||||||
|
isLoading.isTableLoading = true
|
||||||
|
const resp = await xfetch('/api/v1/procedure-src')
|
||||||
|
if (resp.success) {
|
||||||
|
procedures.value = (resp.body as Record<string, any>).data
|
||||||
|
}
|
||||||
|
isLoading.isTableLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const mode = route.query.mode
|
||||||
|
const recordId = route.query['record-id']
|
||||||
|
|
||||||
|
if (mode === 'entry' && recordId) {
|
||||||
|
loadEntryForEdit(+recordId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: mapping data detail when edit
|
||||||
|
const loadEntryForEdit = async (id: number) => {
|
||||||
|
const result = await getDetail(id)
|
||||||
|
|
||||||
|
if (result?.success) {
|
||||||
|
const data = result.body?.data || {}
|
||||||
|
|
||||||
|
const value = JSON.parse(data.value || '{}')
|
||||||
|
model.value.witness1 = value?.witness1 || ''
|
||||||
|
model.value.witness2 = value?.witness2 || ''
|
||||||
|
model.value.informant = value?.informant || ''
|
||||||
|
model.value.responsibleName = value?.responsible || ''
|
||||||
|
model.value.responsiblePhone = value?.responsiblePhone || ''
|
||||||
|
model.value.relatives = value?.relatives || []
|
||||||
|
console.log('model', model.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(type: string) {
|
||||||
|
if (type === 'prosedur') {
|
||||||
|
isOpenProcedure.value = true
|
||||||
|
} else if (type === 'diagnosa') {
|
||||||
|
isOpenDiagnose.value = true
|
||||||
|
} else if (type === 'fungsional') {
|
||||||
|
isOpenDiagnose.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const entryGeneralConsent = ref()
|
||||||
|
async function actionHandler(type: string) {
|
||||||
|
if (type === 'back') {
|
||||||
|
backToList()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (type === 'print') {
|
||||||
|
const data = await getDetail(recordId.value)
|
||||||
|
const detail = data.body?.data
|
||||||
|
fileUrl.value = detail?.fileUrl
|
||||||
|
isOpenDiagnose.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = await entryGeneralConsent.value?.validate()
|
||||||
|
if (result?.valid) {
|
||||||
|
if (result.data.relatives.length > 0) {
|
||||||
|
result.data.relatives = result.data.relatives.map((item: any) => {
|
||||||
|
return item.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('data', result)
|
||||||
|
const resp = await handleActionSave(
|
||||||
|
{
|
||||||
|
...payload.value,
|
||||||
|
value: JSON.stringify(result.data),
|
||||||
|
encounter_id: +route.params.id,
|
||||||
|
},
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
toast,
|
||||||
|
)
|
||||||
|
const data = resp.body?.data
|
||||||
|
if (data) {
|
||||||
|
const resp2 = await create({
|
||||||
|
entityType_code: 'encounter',
|
||||||
|
ref_id: data?.id,
|
||||||
|
type_code: 'general-consent',
|
||||||
|
})
|
||||||
|
console.log('resp2', resp2.body?.data)
|
||||||
|
backToList()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Ada error di form', result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const icdPreview = ref({
|
||||||
|
procedures: [],
|
||||||
|
diagnoses: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
function actionDialogHandler(type: string) {
|
||||||
|
if (type === 'submit') {
|
||||||
|
// icdPreview.value.procedures = selectedProcedure.value || []
|
||||||
|
// icdPreview.value.diagnoses = selectedDiagnose.value || []
|
||||||
|
// icdPreview.value.fungsional = selectedFungsional.value || []
|
||||||
|
}
|
||||||
|
isOpenProcedure.value = false
|
||||||
|
isOpenDiagnose.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
provide('icdPreview', icdPreview)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Entry
|
||||||
|
ref="entryGeneralConsent"
|
||||||
|
v-model="model"
|
||||||
|
:schema="schema"
|
||||||
|
@click="handleClick"
|
||||||
|
/>
|
||||||
|
<div class="my-2 flex justify-end py-2">
|
||||||
|
<Action @click="actionHandler" />
|
||||||
|
</div>
|
||||||
|
<Dialog
|
||||||
|
v-model:open="isOpenDiagnose"
|
||||||
|
title="Preview General Content"
|
||||||
|
size="xl"
|
||||||
|
prevent-outside
|
||||||
|
>
|
||||||
|
<embed
|
||||||
|
style="width: 100%; height: 90vh"
|
||||||
|
:src="fileUrl"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||||
|
import List from '~/components/app/general-consent/list.vue'
|
||||||
|
import Entry from '~/components/app/general-consent/entry.vue'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||||
|
import { GeneralConsentSchema, type GeneralConsentFormData } from '~/schemas/general-consent.schema'
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
import {
|
||||||
|
recId,
|
||||||
|
recAction,
|
||||||
|
recItem,
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleActionEdit,
|
||||||
|
handleActionRemove,
|
||||||
|
handleCancelForm,
|
||||||
|
} from '~/handlers/general-consent.handler'
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { getList, getDetail } from '~/services/general-consent.service'
|
||||||
|
|
||||||
|
// Models
|
||||||
|
import type { Encounter } from '~/models/encounter'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
interface Props {
|
||||||
|
encounter: Encounter
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emits = defineEmits(['add', 'edit'])
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||||
|
|
||||||
|
let units = ref<{ value: string; label: string }[]>([])
|
||||||
|
const encounterId = ref<number>(props?.encounter?.id || 0)
|
||||||
|
const title = ref('')
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
paginationMeta,
|
||||||
|
searchInput,
|
||||||
|
handlePageChange,
|
||||||
|
handleSearch,
|
||||||
|
fetchData: getMyList,
|
||||||
|
} = usePaginatedList({
|
||||||
|
fetchFn: async ({ page, search }) => {
|
||||||
|
const result = await getList({ 'encounter-id': props.encounter.id, includes: '', search, page })
|
||||||
|
if (result.success) {
|
||||||
|
data.value = result.body.data
|
||||||
|
}
|
||||||
|
return { success: result.success || false, body: result.body || {} }
|
||||||
|
},
|
||||||
|
entityName: 'general-consent',
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: 'General Consent',
|
||||||
|
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: () => {
|
||||||
|
goToEntry()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const goEdit = (id: string) => {
|
||||||
|
router.replace({
|
||||||
|
path: route.path,
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
mode: 'entry',
|
||||||
|
'record-id': id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
|
||||||
|
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, () => {
|
||||||
|
console.log('recId', recId.value)
|
||||||
|
if (recAction.value === ActionEvents.showEdit) {
|
||||||
|
goEdit(recId.value)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getMyList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header
|
||||||
|
v-model="searchInput"
|
||||||
|
:prep="headerPrep"
|
||||||
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
|
@search="handleSearch"
|
||||||
|
class="mb-4 xl:mb-5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<List
|
||||||
|
:data="data"
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Record Confirmation Modal -->
|
||||||
|
<RecordConfirmation
|
||||||
|
v-model:open="isRecordConfirmationOpen"
|
||||||
|
action="delete"
|
||||||
|
:record="recItem"
|
||||||
|
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||||
|
@cancel=""
|
||||||
|
>
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>ID:</strong>
|
||||||
|
{{ record?.id }}
|
||||||
|
</p>
|
||||||
|
<p v-if="record?.name">
|
||||||
|
<strong>Nama:</strong>
|
||||||
|
{{ record.name }}
|
||||||
|
</p>
|
||||||
|
<p v-if="record?.code">
|
||||||
|
<strong>Kode:</strong>
|
||||||
|
{{ record.code }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||||
|
import AppMedicineFormList from '~/components/app/medicine-form/list.vue'
|
||||||
|
import AppMedicineFormEntryForm from '~/components/app/medicine-form/entry-form.vue'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||||
|
import { BaseSchema, type BaseFormData } from '~/schemas/base.schema'
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
import {
|
||||||
|
recId,
|
||||||
|
recAction,
|
||||||
|
recItem,
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleActionEdit,
|
||||||
|
handleActionRemove,
|
||||||
|
handleCancelForm,
|
||||||
|
} from '~/handlers/medicine-form.handler'
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { getList, getDetail } from '~/services/medicine-form.service'
|
||||||
|
|
||||||
|
const title = ref('')
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
paginationMeta,
|
||||||
|
searchInput,
|
||||||
|
handlePageChange,
|
||||||
|
handleSearch,
|
||||||
|
fetchData: getMedicineFormList,
|
||||||
|
} = usePaginatedList({
|
||||||
|
fetchFn: async (params: any) => {
|
||||||
|
const result = await getList({
|
||||||
|
search: params.search,
|
||||||
|
sort: 'createdAt:asc',
|
||||||
|
'page-number': params['page-number'] || 0,
|
||||||
|
'page-size': params['page-size'] || 10,
|
||||||
|
})
|
||||||
|
return { success: result.success || false, body: result.body || {} }
|
||||||
|
},
|
||||||
|
entityName: 'medicine-form',
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: 'Sediaan Obat',
|
||||||
|
icon: 'i-lucide-medicine-bottle',
|
||||||
|
refSearchNav: {
|
||||||
|
placeholder: 'Cari (min. 3 karakter)...',
|
||||||
|
minLength: 3,
|
||||||
|
debounceMs: 500,
|
||||||
|
showValidationFeedback: true,
|
||||||
|
onInput: (value: string) => {
|
||||||
|
searchInput.value = value
|
||||||
|
},
|
||||||
|
onClick: () => {},
|
||||||
|
onClear: () => {},
|
||||||
|
},
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah',
|
||||||
|
icon: 'i-lucide-plus',
|
||||||
|
onClick: () => {
|
||||||
|
recItem.value = null
|
||||||
|
recId.value = 0
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
isReadonly.value = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
|
||||||
|
const getCurrentMedicineFormDetail = async (id: number | string) => {
|
||||||
|
const result = await getDetail(id)
|
||||||
|
if (result.success) {
|
||||||
|
const currentValue = result.body?.data || {}
|
||||||
|
recItem.value = currentValue
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for row actions when recId or recAction changes
|
||||||
|
watch([recId, recAction], () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showDetail:
|
||||||
|
getCurrentMedicineFormDetail(recId.value)
|
||||||
|
title.value = 'Detail Sediaan Obat'
|
||||||
|
isReadonly.value = true
|
||||||
|
break
|
||||||
|
case ActionEvents.showEdit:
|
||||||
|
getCurrentMedicineFormDetail(recId.value)
|
||||||
|
title.value = 'Edit Sediaan Obat'
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
isReadonly.value = false
|
||||||
|
break
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getMedicineFormList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header
|
||||||
|
v-model="searchInput"
|
||||||
|
:prep="headerPrep"
|
||||||
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AppMedicineFormList
|
||||||
|
:data="data"
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
v-model:open="isFormEntryDialogOpen"
|
||||||
|
:title="!!recItem ? title : 'Tambah Sediaan Obat'"
|
||||||
|
size="lg"
|
||||||
|
prevent-outside
|
||||||
|
@update:open="
|
||||||
|
(value: any) => {
|
||||||
|
onResetState()
|
||||||
|
isFormEntryDialogOpen = value
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<AppMedicineFormEntryForm
|
||||||
|
:schema="BaseSchema"
|
||||||
|
:values="recItem"
|
||||||
|
:is-loading="isProcessing"
|
||||||
|
:is-readonly="isReadonly"
|
||||||
|
@submit="
|
||||||
|
(values: BaseFormData | Record<string, any>, resetForm: () => void) => {
|
||||||
|
if (recId > 0) {
|
||||||
|
handleActionEdit(recItem.code, values, getMedicineFormList, resetForm, toast)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleActionSave(values, getMedicineFormList, resetForm, toast)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@cancel="handleCancelForm"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Record Confirmation Modal -->
|
||||||
|
<RecordConfirmation
|
||||||
|
v-model:open="isRecordConfirmationOpen"
|
||||||
|
action="delete"
|
||||||
|
:record="recItem"
|
||||||
|
@confirm="() => handleActionRemove(recItem.code, getMedicineFormList, toast)"
|
||||||
|
@cancel=""
|
||||||
|
>
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>ID:</strong>
|
||||||
|
{{ record?.id }}
|
||||||
|
</p>
|
||||||
|
<p v-if="record?.name">
|
||||||
|
<strong>Nama:</strong>
|
||||||
|
{{ record.name }}
|
||||||
|
</p>
|
||||||
|
<p v-if="record?.code">
|
||||||
|
<strong>Kode:</strong>
|
||||||
|
{{ record.code }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</template>
|
||||||
@@ -108,6 +108,7 @@ watch([recId, recAction], () => {
|
|||||||
getCurrentMedicineGroupDetail(recId.value)
|
getCurrentMedicineGroupDetail(recId.value)
|
||||||
title.value = 'Edit Kelompok Obat'
|
title.value = 'Edit Kelompok Obat'
|
||||||
isReadonly.value = false
|
isReadonly.value = false
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
break
|
break
|
||||||
case ActionEvents.showConfirmDelete:
|
case ActionEvents.showConfirmDelete:
|
||||||
isRecordConfirmationOpen.value = true
|
isRecordConfirmationOpen.value = true
|
||||||
@@ -154,7 +155,7 @@ onMounted(async () => {
|
|||||||
@submit="
|
@submit="
|
||||||
(values: BaseFormData | Record<string, any>, resetForm: () => void) => {
|
(values: BaseFormData | Record<string, any>, resetForm: () => void) => {
|
||||||
if (recId > 0) {
|
if (recId > 0) {
|
||||||
handleActionEdit(recId, values, getMedicineGroupList, resetForm, toast)
|
handleActionEdit(recItem.code, values, getMedicineGroupList, resetForm, toast)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleActionSave(values, getMedicineGroupList, resetForm, toast)
|
handleActionSave(values, getMedicineGroupList, resetForm, toast)
|
||||||
@@ -169,7 +170,7 @@ onMounted(async () => {
|
|||||||
v-model:open="isRecordConfirmationOpen"
|
v-model:open="isRecordConfirmationOpen"
|
||||||
action="delete"
|
action="delete"
|
||||||
:record="recItem"
|
:record="recItem"
|
||||||
@confirm="() => handleActionRemove(recId, getMedicineGroupList, toast)"
|
@confirm="() => handleActionRemove(recItem.code, getMedicineGroupList, toast)"
|
||||||
@cancel=""
|
@cancel=""
|
||||||
>
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ watch([recId, recAction], () => {
|
|||||||
getCurrentMedicineMethodDetail(recId.value)
|
getCurrentMedicineMethodDetail(recId.value)
|
||||||
title.value = 'Edit Metode Obat'
|
title.value = 'Edit Metode Obat'
|
||||||
isReadonly.value = false
|
isReadonly.value = false
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
break
|
break
|
||||||
case ActionEvents.showConfirmDelete:
|
case ActionEvents.showConfirmDelete:
|
||||||
isRecordConfirmationOpen.value = true
|
isRecordConfirmationOpen.value = true
|
||||||
@@ -154,7 +155,7 @@ onMounted(async () => {
|
|||||||
@submit="
|
@submit="
|
||||||
(values: BaseFormData | Record<string, any>, resetForm: () => void) => {
|
(values: BaseFormData | Record<string, any>, resetForm: () => void) => {
|
||||||
if (recId > 0) {
|
if (recId > 0) {
|
||||||
handleActionEdit(recId, values, getMedicineMethodList, resetForm, toast)
|
handleActionEdit(recItem.code, values, getMedicineMethodList, resetForm, toast)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleActionSave(values, getMedicineMethodList, resetForm, toast)
|
handleActionSave(values, getMedicineMethodList, resetForm, toast)
|
||||||
@@ -169,7 +170,7 @@ onMounted(async () => {
|
|||||||
v-model:open="isRecordConfirmationOpen"
|
v-model:open="isRecordConfirmationOpen"
|
||||||
action="delete"
|
action="delete"
|
||||||
:record="recItem"
|
:record="recItem"
|
||||||
@confirm="() => handleActionRemove(recId, getMedicineMethodList, toast)"
|
@confirm="() => handleActionRemove(recItem.code, getMedicineMethodList, toast)"
|
||||||
@cancel=""
|
@cancel=""
|
||||||
>
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
|
|||||||
@@ -37,10 +37,12 @@ import {
|
|||||||
import { getList, getDetail } from '~/services/medicine.service'
|
import { getList, getDetail } from '~/services/medicine.service'
|
||||||
import { getValueLabelList as getMedicineGroupList } from '~/services/medicine-group.service'
|
import { getValueLabelList as getMedicineGroupList } from '~/services/medicine-group.service'
|
||||||
import { getValueLabelList as getMedicineMethodList } from '~/services/medicine-method.service'
|
import { getValueLabelList as getMedicineMethodList } from '~/services/medicine-method.service'
|
||||||
|
import { getValueLabelList as getMedicineFormList } from '~/services/medicine-form.service'
|
||||||
import { getValueLabelList as getUomList } from '~/services/uom.service'
|
import { getValueLabelList as getUomList } from '~/services/uom.service'
|
||||||
|
|
||||||
const medicineGroups = ref<{ value: string; label: string }[]>([])
|
const medicineGroups = ref<{ value: string; label: string }[]>([])
|
||||||
const medicineMethods = ref<{ value: string; label: string }[]>([])
|
const medicineMethods = ref<{ value: string; label: string }[]>([])
|
||||||
|
const medicineForms = ref<{ value: string; label: string }[]>([])
|
||||||
const uoms = ref<{ value: string; label: string }[]>([])
|
const uoms = ref<{ value: string; label: string }[]>([])
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@ const {
|
|||||||
sort: 'createdAt:asc',
|
sort: 'createdAt:asc',
|
||||||
'page-number': params['page-number'] || 0,
|
'page-number': params['page-number'] || 0,
|
||||||
'page-size': params['page-size'] || 10,
|
'page-size': params['page-size'] || 10,
|
||||||
includes: 'medicineGroup,medicineMethod,uom',
|
includes: 'medicineGroup,medicineMethod,medicineForm,uom',
|
||||||
})
|
})
|
||||||
return { success: result.success || false, body: result.body || {} }
|
return { success: result.success || false, body: result.body || {} }
|
||||||
},
|
},
|
||||||
@@ -116,6 +118,7 @@ watch([recId, recAction], () => {
|
|||||||
case ActionEvents.showEdit:
|
case ActionEvents.showEdit:
|
||||||
getCurrentMedicineDetail(recId.value)
|
getCurrentMedicineDetail(recId.value)
|
||||||
title.value = 'Edit Obat'
|
title.value = 'Edit Obat'
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
isReadonly.value = false
|
isReadonly.value = false
|
||||||
break
|
break
|
||||||
case ActionEvents.showConfirmDelete:
|
case ActionEvents.showConfirmDelete:
|
||||||
@@ -127,6 +130,7 @@ watch([recId, recAction], () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
medicineGroups.value = await getMedicineGroupList({ sort: 'createdAt:asc', 'page-size': 100 })
|
medicineGroups.value = await getMedicineGroupList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||||
medicineMethods.value = await getMedicineMethodList({ sort: 'createdAt:asc', 'page-size': 100 })
|
medicineMethods.value = await getMedicineMethodList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||||
|
medicineForms.value = await getMedicineFormList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||||
uoms.value = await getUomList({ sort: 'createdAt:asc', 'page-size': 100 })
|
uoms.value = await getUomList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||||
await getMedicineList()
|
await getMedicineList()
|
||||||
})
|
})
|
||||||
@@ -163,13 +167,14 @@ onMounted(async () => {
|
|||||||
:values="recItem"
|
:values="recItem"
|
||||||
:medicineGroups="medicineGroups"
|
:medicineGroups="medicineGroups"
|
||||||
:medicineMethods="medicineMethods"
|
:medicineMethods="medicineMethods"
|
||||||
|
:medicineForms="medicineForms"
|
||||||
:uoms="uoms"
|
:uoms="uoms"
|
||||||
:is-loading="isProcessing"
|
:is-loading="isProcessing"
|
||||||
:is-readonly="isReadonly"
|
:is-readonly="isReadonly"
|
||||||
@submit="
|
@submit="
|
||||||
(values: MedicineFormData | Record<string, any>, resetForm: () => void) => {
|
(values: MedicineFormData | Record<string, any>, resetForm: () => void) => {
|
||||||
if (recId > 0) {
|
if (recId > 0) {
|
||||||
handleActionEdit(recId, values, getMedicineList, resetForm, toast)
|
handleActionEdit(recItem.code, values, getMedicineList, resetForm, toast)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleActionSave(values, getMedicineList, resetForm, toast)
|
handleActionSave(values, getMedicineList, resetForm, toast)
|
||||||
@@ -184,7 +189,7 @@ onMounted(async () => {
|
|||||||
v-model:open="isRecordConfirmationOpen"
|
v-model:open="isRecordConfirmationOpen"
|
||||||
action="delete"
|
action="delete"
|
||||||
:record="recItem"
|
:record="recItem"
|
||||||
@confirm="() => handleActionRemove(recId, getMedicineList, toast)"
|
@confirm="() => handleActionRemove(recItem.code, getMedicineList, toast)"
|
||||||
@cancel=""
|
@cancel=""
|
||||||
>
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ExposedForm } from '~/types/form';
|
||||||
|
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||||
|
import { ResumeSchema } from '~/schemas/resume.schema';
|
||||||
|
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||||
|
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date';
|
||||||
|
import type { DateRange } from 'radix-vue';
|
||||||
|
import { getPatients } from '~/services/patient.service';
|
||||||
|
import ActionHistoryDialog from '~/components/app/resume/history-list/action-history-dialog.vue';
|
||||||
|
import ConsultationHistoryDialog from '~/components/app/resume/history-list/consultation-history-dialog.vue';
|
||||||
|
import SupportingHistoryDialog from '~/components/app/resume/history-list/supporting-history-dialog.vue';
|
||||||
|
import FarmacyHistoryDialog from '~/components/app/resume/history-list/farmacy-history-dialog.vue';
|
||||||
|
import NationalProgramHistoryDialog from '~/components/app/resume/history-list/national-program-history-dialog.vue';
|
||||||
|
|
||||||
|
// #region Props & Emits
|
||||||
|
const props = defineProps<{
|
||||||
|
callbackUrl?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// form related state
|
||||||
|
const personPatientForm = ref<ExposedForm<any> | null>(null)
|
||||||
|
const actionHistoryData = usePaginatedList({
|
||||||
|
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||||
|
entityName: 'patient',
|
||||||
|
})
|
||||||
|
const consultationHistoryData = usePaginatedList({
|
||||||
|
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||||
|
entityName: 'patient',
|
||||||
|
})
|
||||||
|
const supportingHistoryData = usePaginatedList({
|
||||||
|
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||||
|
entityName: 'patient',
|
||||||
|
})
|
||||||
|
const farmacyHistoryData = usePaginatedList({
|
||||||
|
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||||
|
entityName: 'patient',
|
||||||
|
})
|
||||||
|
const nationalProgramServiceHistoryData = usePaginatedList({
|
||||||
|
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||||
|
entityName: 'patient',
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region State & Computed
|
||||||
|
const router = useRouter()
|
||||||
|
const isConfirmationOpen = ref(false)
|
||||||
|
const isActionHistoryOpen = ref<boolean>(false)
|
||||||
|
const isConsultationHistoryOpen = ref<boolean>(false)
|
||||||
|
const isSupportingHistoryOpen = ref<boolean>(false)
|
||||||
|
const isFarmacyHistoryOpen = ref<boolean>(false)
|
||||||
|
const isNationalProgramServiceHistoryOpen = ref<boolean>(false)
|
||||||
|
|
||||||
|
provide(`isActionHistoryOpen`, isActionHistoryOpen)
|
||||||
|
provide(`isConsultationHistoryOpen`, isConsultationHistoryOpen)
|
||||||
|
provide(`isSupportingHistoryOpen`, isSupportingHistoryOpen)
|
||||||
|
provide(`isFarmacyHistoryOpen`, isFarmacyHistoryOpen)
|
||||||
|
provide(`isNationalProgramServiceHistoryOpen`, isNationalProgramServiceHistoryOpen)
|
||||||
|
|
||||||
|
const defaultDate = {
|
||||||
|
start: new CalendarDate(2022, 1, 20),
|
||||||
|
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionHistoryDateValue = ref(defaultDate) as Ref<DateRange>
|
||||||
|
const consultationHistoryDateValue = ref(defaultDate) as Ref<DateRange>
|
||||||
|
const supportingHistoryDateValue = ref(defaultDate) as Ref<DateRange>
|
||||||
|
const farmacyHistoryDateValue = ref(defaultDate) as Ref<DateRange>
|
||||||
|
const nationalProgramServiceSearch = ref<string>('')
|
||||||
|
const nationalProgramServiceSelectedStatus = ref<string>('all')
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle Hooks
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
function goBack() {
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmAdd() {
|
||||||
|
// handleActionClick('submit')
|
||||||
|
console.log(`tersubmit wak`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelAdd() {
|
||||||
|
isConfirmationOpen.value = false
|
||||||
|
}
|
||||||
|
// #endregion region
|
||||||
|
|
||||||
|
// #region Utilities & event handlers
|
||||||
|
async function handleActionClick(eventType: string) {
|
||||||
|
if (eventType === 'submit') {
|
||||||
|
isConfirmationOpen.value = true
|
||||||
|
// const patient: Patient = await composeFormData()
|
||||||
|
// let createdPatientId = 0
|
||||||
|
|
||||||
|
// const response = await handleActionSave(
|
||||||
|
// patient,
|
||||||
|
// () => {},
|
||||||
|
// () => {},
|
||||||
|
// toast,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const data = (response?.body?.data ?? null) as PatientBase | null
|
||||||
|
// if (!data) return
|
||||||
|
// createdPatientId = data.id
|
||||||
|
|
||||||
|
// If has callback provided redirect to callback with patientData
|
||||||
|
// if (props.callbackUrl) {
|
||||||
|
// await navigateTo(props.callbackUrl + '?patient-id=' + patient.id)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Navigate to patient list or show success message
|
||||||
|
// await navigateTo('/outpatient/encounter')
|
||||||
|
// return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'back') {
|
||||||
|
if (props.callbackUrl) {
|
||||||
|
await navigateTo(props.callbackUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack()
|
||||||
|
// handleCancelForm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Tambah Resume</div>
|
||||||
|
<AppResumeAdd
|
||||||
|
ref="personPatientForm"
|
||||||
|
:schema="ResumeSchema"
|
||||||
|
:resume-arrangement-type="personPatientForm?.values.arrangement"/>
|
||||||
|
|
||||||
|
<div class="my-2 flex justify-end py-2">
|
||||||
|
<Action :enable-draft="false"
|
||||||
|
@click="handleActionClick"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Confirmation
|
||||||
|
v-model:open="isConfirmationOpen"
|
||||||
|
title="Simpan Data"
|
||||||
|
message="Apakah Anda yakin ingin menyimpan data ini?"
|
||||||
|
confirm-text="Simpan"
|
||||||
|
@confirm="handleConfirmAdd"
|
||||||
|
@cancel="handleCancelAdd"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActionHistoryDialog
|
||||||
|
v-model:is-modal-open="isActionHistoryOpen"
|
||||||
|
v-model:date-value="actionHistoryDateValue"
|
||||||
|
:data="actionHistoryData.data.value"
|
||||||
|
:pagination-meta="actionHistoryData.paginationMeta"
|
||||||
|
@page-change="actionHistoryData.handlePageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p v-if="isConsultationHistoryOpen === true">aaaaaaaaaaaaaaa</p>
|
||||||
|
<ConsultationHistoryDialog
|
||||||
|
v-model:is-modal-open="isConsultationHistoryOpen"
|
||||||
|
v-model:date-value="consultationHistoryDateValue"
|
||||||
|
:data="consultationHistoryData.data.value"
|
||||||
|
:pagination-meta="consultationHistoryData.paginationMeta"
|
||||||
|
@page-change="consultationHistoryData.handlePageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SupportingHistoryDialog
|
||||||
|
v-model:is-modal-open="isSupportingHistoryOpen"
|
||||||
|
v-model:date-value="supportingHistoryDateValue"
|
||||||
|
:data="supportingHistoryData.data.value"
|
||||||
|
:pagination-meta="supportingHistoryData.paginationMeta"
|
||||||
|
@page-change="supportingHistoryData.handlePageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FarmacyHistoryDialog
|
||||||
|
v-model:is-modal-open="isFarmacyHistoryOpen"
|
||||||
|
v-model:date-value="farmacyHistoryDateValue"
|
||||||
|
:data="farmacyHistoryData.data.value"
|
||||||
|
:pagination-meta="farmacyHistoryData.paginationMeta"
|
||||||
|
@page-change="farmacyHistoryData.handlePageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NationalProgramHistoryDialog
|
||||||
|
v-model:is-modal-open="isNationalProgramServiceHistoryOpen"
|
||||||
|
v-model:search-value="nationalProgramServiceSearch"
|
||||||
|
v-model:status-value="nationalProgramServiceSelectedStatus"
|
||||||
|
:data="nationalProgramServiceHistoryData.data.value"
|
||||||
|
:pagination-meta="nationalProgramServiceHistoryData.paginationMeta"
|
||||||
|
@page-change="nationalProgramServiceHistoryData.handlePageChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||||
|
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
|
||||||
|
|
||||||
|
// #region Imports
|
||||||
|
import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
|
||||||
|
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||||
|
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||||
|
|
||||||
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
import SummaryCard from '~/components/pub/my-ui/summary-card/summary-card.vue'
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||||
|
|
||||||
|
import { getPatients, removePatient } from '~/services/patient.service'
|
||||||
|
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
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 { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||||
|
import { unauthorizedToast } from '~/lib/utils'
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
|
||||||
|
// #region Permission
|
||||||
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
|
||||||
|
const { getPagePermissions } = useRBAC()
|
||||||
|
const pagePermission = getPagePermissions(roleAccess)
|
||||||
|
|
||||||
|
// #region State
|
||||||
|
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
|
||||||
|
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||||
|
entityName: 'patient',
|
||||||
|
})
|
||||||
|
|
||||||
|
const refSearchNav: RefSearchNav = {
|
||||||
|
onClick: () => {
|
||||||
|
// open filter modal
|
||||||
|
},
|
||||||
|
onInput: (val: string) => {
|
||||||
|
searchInput.value = val
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
searchInput.value = ''
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const verificationInputForm = ref<ExposedForm<any> | null>(null)
|
||||||
|
const isVerifyDialogOpen = ref(false)
|
||||||
|
const isDocPreviewDialogOpen = ref(false)
|
||||||
|
const isRecordConfirmationOpen = ref(false)
|
||||||
|
const summaryLoading = ref(false)
|
||||||
|
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
const isCaptchaValid = ref(false)
|
||||||
|
provide('isCaptchaValid', isCaptchaValid)
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: "Resume",
|
||||||
|
icon: 'i-lucide-newspaper',
|
||||||
|
}
|
||||||
|
if (pagePermission.canCreate) {
|
||||||
|
headerPrep.addNav = {
|
||||||
|
label: "Resume",
|
||||||
|
onClick: () => navigateTo('/resume/add'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle Hooks
|
||||||
|
onMounted(() => {
|
||||||
|
getPatientSummary()
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
async function getPatientSummary() {
|
||||||
|
try {
|
||||||
|
summaryLoading.value = true
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching patient summary:', error)
|
||||||
|
} finally {
|
||||||
|
summaryLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleActionClick(eventType: string) {
|
||||||
|
if (eventType === 'submit') {
|
||||||
|
// const patient: Patient = await composeFormData()
|
||||||
|
// let createdPatientId = 0
|
||||||
|
|
||||||
|
// const response = await handleActionSave(
|
||||||
|
// patient,
|
||||||
|
// () => {},
|
||||||
|
// () => {},
|
||||||
|
// toast,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const data = (response?.body?.data ?? null) as PatientBase | null
|
||||||
|
// if (!data) return
|
||||||
|
// createdPatientId = data.id
|
||||||
|
|
||||||
|
// If has callback provided redirect to callback with patientData
|
||||||
|
// if (props.callbackUrl) {
|
||||||
|
// await navigateTo(props.callbackUrl + '?patient-id=' + patient.id)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Navigate to patient list or show success message
|
||||||
|
// await navigateTo('/outpatient/encounter')
|
||||||
|
// return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'back') {
|
||||||
|
isVerifyDialogOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmDelete() {
|
||||||
|
try {
|
||||||
|
const result = await removePatient(recId.value)
|
||||||
|
if (result.success) {
|
||||||
|
console.log('Patient deleted successfully')
|
||||||
|
// Refresh the list
|
||||||
|
await fetchData()
|
||||||
|
} else {
|
||||||
|
console.error('Failed to delete patient:', result)
|
||||||
|
// Handle error - show error message to user
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting patient:', error)
|
||||||
|
// Handle error - show error message to user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelConfirmation() {
|
||||||
|
// Reset record state when cancelled
|
||||||
|
recId.value = 0
|
||||||
|
recAction.value = ''
|
||||||
|
recItem.value = null
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Provide
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
watch([recId, recAction], () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showVerify:
|
||||||
|
if(pagePermission.canUpdate) {
|
||||||
|
isVerifyDialogOpen.value = true
|
||||||
|
} else {
|
||||||
|
unauthorizedToast()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ActionEvents.showValidate:
|
||||||
|
if(pagePermission.canUpdate) {
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
} else {
|
||||||
|
unauthorizedToast()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ActionEvents.showPrint:
|
||||||
|
isDocPreviewDialogOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header :prep="{ ...headerPrep }" />
|
||||||
|
|
||||||
|
<!-- <AppTherapyProtocolList
|
||||||
|
:data="data"
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@page-change="handlePageChange"/> -->
|
||||||
|
<AppResumeList
|
||||||
|
:data="data"
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@page-change="handlePageChange"/>
|
||||||
|
|
||||||
|
<Dialog v-model:open="isVerifyDialogOpen" title="Verifikasi">
|
||||||
|
<AppResumeVerifyDialog
|
||||||
|
ref="verificationInputForm"
|
||||||
|
:schema="VerificationSchema" />
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Action v-show="isCaptchaValid" :enable-draft="false" @click="handleActionClick" />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog v-model:open="isDocPreviewDialogOpen" title="Preview Dokumen" size="2xl">
|
||||||
|
<DocPreviewDialog :link="`https://www.antennahouse.com/hubfs/xsl-fo-sample/pdf/basic-link-1.pdf`" />
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Confirmation
|
||||||
|
v-model:open="isRecordConfirmationOpen"
|
||||||
|
title="Validasi Data"
|
||||||
|
message="Apakah Anda yakin ingin menvalidasi data ini?"
|
||||||
|
confirm-text="Validasi"
|
||||||
|
@confirm="handleConfirmDelete"
|
||||||
|
@cancel="handleCancelConfirmation"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -163,7 +163,7 @@ onMounted(async () => {
|
|||||||
@submit="
|
@submit="
|
||||||
(values: DeviceFormData, resetForm: any) => {
|
(values: DeviceFormData, resetForm: any) => {
|
||||||
if (recId > 0) {
|
if (recId > 0) {
|
||||||
handleActionEdit(recId, values, getToolsList, resetForm, toast)
|
handleActionEdit(recItem.code, values, getToolsList, resetForm, toast)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleActionSave(values, getToolsList, resetForm, toast)
|
handleActionSave(values, getToolsList, resetForm, toast)
|
||||||
@@ -178,7 +178,7 @@ onMounted(async () => {
|
|||||||
v-model:open="isRecordConfirmationOpen"
|
v-model:open="isRecordConfirmationOpen"
|
||||||
action="delete"
|
action="delete"
|
||||||
:record="recItem"
|
:record="recItem"
|
||||||
@confirm="() => handleActionRemove(recId, getToolsList, toast)"
|
@confirm="() => handleActionRemove(recItem.code, getToolsList, toast)"
|
||||||
@cancel=""
|
@cancel=""
|
||||||
>
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { LinkItem, ListItemDto } from './types'
|
||||||
|
import { ActionEvents } from './types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rec: ListItemDto
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const recId = inject<Ref<number>>('rec_id')!
|
||||||
|
const recAction = inject<Ref<string>>('rec_action')!
|
||||||
|
const recItem = inject<Ref<any>>('rec_item')!
|
||||||
|
const timestamp = inject<Ref<number>>('timestamp')!
|
||||||
|
const activeKey = ref<string | null>(null)
|
||||||
|
const linkItems: LinkItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Detail',
|
||||||
|
onClick: () => {
|
||||||
|
detail()
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-eye',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hapus',
|
||||||
|
onClick: () => {
|
||||||
|
del()
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-trash',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function detail() {
|
||||||
|
recId.value = props.rec.id || 0
|
||||||
|
recAction.value = ActionEvents.showDetail
|
||||||
|
recItem.value = props.rec
|
||||||
|
timestamp.value = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
function del() {
|
||||||
|
recId.value = props.rec.id || 0
|
||||||
|
recAction.value = ActionEvents.showConfirmDelete
|
||||||
|
recItem.value = props.rec
|
||||||
|
timestamp.value = Date.now()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="i-lucide-chevrons-up-down"
|
||||||
|
class="ml-auto size-4"
|
||||||
|
/>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-for="item in linkItems"
|
||||||
|
:key="item.label"
|
||||||
|
class="hover:bg-gray-100 dark:hover:bg-slate-700"
|
||||||
|
@click="item.onClick"
|
||||||
|
@mouseenter="activeKey = item.label"
|
||||||
|
@mouseleave="activeKey = null"
|
||||||
|
>
|
||||||
|
<Icon :name="item.icon ?? ''" />
|
||||||
|
<span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -2,14 +2,9 @@
|
|||||||
import type { LinkItem, ListItemDto } from './types'
|
import type { LinkItem, ListItemDto } from './types'
|
||||||
import { ActionEvents } from './types'
|
import { ActionEvents } from './types'
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
rec: ListItemDto
|
rec: ListItemDto
|
||||||
size?: 'default' | 'sm' | 'lg'
|
}>()
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
size: 'lg',
|
|
||||||
})
|
|
||||||
|
|
||||||
const recId = inject<Ref<number>>('rec_id')!
|
const recId = inject<Ref<number>>('rec_id')!
|
||||||
const recAction = inject<Ref<string>>('rec_action')!
|
const recAction = inject<Ref<string>>('rec_action')!
|
||||||
@@ -63,7 +58,7 @@ function del() {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
:size="size"
|
size="lg"
|
||||||
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
|
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { LinkItem, ListItemDto } from './types'
|
||||||
|
import { ActionEvents } from './types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rec: ListItemDto
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const recId = inject<Ref<number>>('rec_id')!
|
||||||
|
const recAction = inject<Ref<string>>('rec_action')!
|
||||||
|
const recItem = inject<Ref<any>>('rec_item')!
|
||||||
|
const activeKey = ref<string | null>(null)
|
||||||
|
const linkItems: LinkItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Detail',
|
||||||
|
onClick: () => {
|
||||||
|
detail()
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-eye',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Verifikasi',
|
||||||
|
onClick: () => {
|
||||||
|
verify()
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-check',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Validasi',
|
||||||
|
onClick: () => {
|
||||||
|
validate()
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-check-check',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Print',
|
||||||
|
onClick: () => {
|
||||||
|
print()
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-printer',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function detail() {
|
||||||
|
recId.value = props.rec.id || 0
|
||||||
|
recAction.value = ActionEvents.showDetail
|
||||||
|
recItem.value = props.rec
|
||||||
|
}
|
||||||
|
|
||||||
|
function verify() {
|
||||||
|
recId.value = props.rec.id || 0
|
||||||
|
recAction.value = ActionEvents.showVerify
|
||||||
|
recItem.value = props.rec
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
recId.value = props.rec.id || 0
|
||||||
|
recAction.value = ActionEvents.showValidate
|
||||||
|
recItem.value = props.rec
|
||||||
|
}
|
||||||
|
|
||||||
|
function print() {
|
||||||
|
recId.value = props.rec.id || 0
|
||||||
|
recAction.value = ActionEvents.showPrint
|
||||||
|
recItem.value = props.rec
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="i-lucide-chevrons-up-down"
|
||||||
|
class="ml-auto size-4"
|
||||||
|
/>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-for="item in linkItems"
|
||||||
|
:key="item.label"
|
||||||
|
class="hover:bg-gray-100 dark:hover:bg-slate-700"
|
||||||
|
@click="item.onClick"
|
||||||
|
@mouseenter="activeKey = item.label"
|
||||||
|
@mouseleave="activeKey = null"
|
||||||
|
>
|
||||||
|
<Icon :name="item.icon ?? ''" />
|
||||||
|
<span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -78,6 +78,9 @@ export const ActionEvents = {
|
|||||||
showEdit: 'showEdit',
|
showEdit: 'showEdit',
|
||||||
showDetail: 'showDetail',
|
showDetail: 'showDetail',
|
||||||
showProcess: 'showProcess',
|
showProcess: 'showProcess',
|
||||||
|
showVerify: 'showVerify',
|
||||||
|
showValidate: 'showValidate',
|
||||||
|
showPrint: 'showPrint',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataTableLoader {
|
export interface DataTableLoader {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ async function onFileChange(event: Event, handleChange: (value: any) => void) {
|
|||||||
@change="onFileChange($event, handleChange)"
|
@change="onFileChange($event, handleChange)"
|
||||||
type="file"
|
type="file"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
v-bind="componentField"
|
v-bind="{ onBlur: componentField.onBlur }"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -63,14 +63,14 @@ function handleInput(event: Event) {
|
|||||||
v-slot="{ componentField }"
|
v-slot="{ componentField }"
|
||||||
:name="fieldName"
|
:name="fieldName"
|
||||||
>
|
>
|
||||||
<FormItem :class="`relative`">
|
<FormItem :class="cn(`relative`,)">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:maxlength="maxLength"
|
:maxlength="maxLength"
|
||||||
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0', props.class)"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
aria-autocomplete="none"
|
aria-autocomplete="none"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
@@ -84,6 +84,6 @@ function handleInput(event: Event) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</DE.Field>
|
</DE.Field>
|
||||||
<p v-show="bottomLabel" class="text-gray-400">{{ bottomLabel }}</p>
|
<p v-show="bottomLabel" class="text-gray-400 mt-1">{{ bottomLabel }}</p>
|
||||||
</DE.Cell>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||||
|
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||||
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
|
import { Input } from '~/components/pub/ui/input'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName: string
|
||||||
|
placeholder: string
|
||||||
|
label: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
colSpan?: number
|
||||||
|
numericOnly?: boolean
|
||||||
|
maxLength?: number
|
||||||
|
isRequired?: boolean
|
||||||
|
isDisabled?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handleInput(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
let value = target.value
|
||||||
|
|
||||||
|
// Filter numeric only jika diperlukan
|
||||||
|
if (props.numericOnly) {
|
||||||
|
value = value.replace(/\D/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batasi panjang maksimal jika diperlukan
|
||||||
|
if (props.maxLength && value.length > props.maxLength) {
|
||||||
|
value = value.slice(0, props.maxLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update value jika ada perubahan
|
||||||
|
if (target.value !== value) {
|
||||||
|
target.value = value
|
||||||
|
// Trigger input event untuk update form
|
||||||
|
target.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :col-span="colSpan || 1">
|
||||||
|
<DE.Label
|
||||||
|
class="mb-1"
|
||||||
|
v-if="label !== ''"
|
||||||
|
:label-for="fieldName"
|
||||||
|
:is-required="isRequired && !isDisabled"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
:disabled="isDisabled"
|
||||||
|
v-bind="componentField"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:maxlength="maxLength"
|
||||||
|
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
||||||
|
autocomplete="off"
|
||||||
|
aria-autocomplete="none"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, defineEmits, defineProps, onMounted, nextTick, defineExpose } from 'vue'
|
||||||
|
import Input from '~/components/pub/ui/input/Input.vue';
|
||||||
|
import Button from '~/components/pub/ui/button/Button.vue';
|
||||||
|
import waveyFingerprint from '~/assets/svg/wavey-fingerprint.svg'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextCaptcha props:
|
||||||
|
* - length: number of characters in the core captcha
|
||||||
|
* - caseSensitive: whether validation is case sensitive
|
||||||
|
* - useSpacing: show spaced-out characters (visual obfuscation only)
|
||||||
|
* - noiseChars: include random noise characters visually (not required to type)
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
length: { type: Number, default: 6 },
|
||||||
|
caseSensitive: { type: Boolean, default: false },
|
||||||
|
useSpacing: { type: Boolean, default: true },
|
||||||
|
noiseChars: { type: Boolean, default: false }, // adds random noise characters to display
|
||||||
|
refreshCooldownMs: { type: Number, default: 500 }, // guard repeated refresh
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:valid', valid: boolean): void
|
||||||
|
(e: 'validated', valid: boolean): void
|
||||||
|
(e: 'change', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
const raw = ref('') // the canonical captcha value (what user must match, ignoring visual noise)
|
||||||
|
const display = ref('') // randomized visual representation (may include spacing/noise)
|
||||||
|
const input = ref('') // user typed value
|
||||||
|
const lastRefresh = ref(0)
|
||||||
|
const valid = inject('isCaptchaValid') as Ref<boolean>
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
/** Characters excluding ambiguous ones: 0/O, 1/l/I etc. */
|
||||||
|
const CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||||
|
|
||||||
|
function randomChar() {
|
||||||
|
return CHARS.charAt(Math.floor(Math.random() * CHARS.length))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate the canonical captcha string */
|
||||||
|
function genRaw(len = props.length) {
|
||||||
|
let s = ''
|
||||||
|
for (let i = 0; i < len; i++) s += randomChar()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a visually obfuscated display string (spacing, noise, random case) */
|
||||||
|
function genDisplay(base: string) {
|
||||||
|
const arr: string[] = []
|
||||||
|
for (const ch of base) {
|
||||||
|
// toggle case randomly (only for letters)
|
||||||
|
const c = /[A-Za-z]/.test(ch) && Math.random() > 0.5 ? (Math.random() > 0.5 ? ch.toLowerCase() : ch.toUpperCase()) : ch
|
||||||
|
arr.push(c)
|
||||||
|
if (props.useSpacing && Math.random() > 0.3) arr.push(' ') // random space
|
||||||
|
}
|
||||||
|
return arr.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Refresh captcha */
|
||||||
|
function refresh() {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastRefresh.value < props.refreshCooldownMs) return
|
||||||
|
lastRefresh.value = now
|
||||||
|
|
||||||
|
raw.value = genRaw(props.length)
|
||||||
|
display.value = genDisplay(raw.value)
|
||||||
|
input.value = ''
|
||||||
|
valid.value = false
|
||||||
|
errorMessage.value = ''
|
||||||
|
// emit change so parent knows new value (but we don't send the raw canonical in production)
|
||||||
|
emit('change', display.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Normalize input and canonical for comparison */
|
||||||
|
function normalizeForCompare(s: string) {
|
||||||
|
const normalized = s.replace(/\s+/g, '') // strip spaces
|
||||||
|
return props.caseSensitive ? normalized : normalized.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validate the current input */
|
||||||
|
function validate() {
|
||||||
|
const left = normalizeForCompare(input.value)
|
||||||
|
const right = normalizeForCompare(raw.value)
|
||||||
|
if (!input.value) {
|
||||||
|
valid.value = false
|
||||||
|
errorMessage.value = 'Please enter the captcha text.'
|
||||||
|
} else if (left === right) {
|
||||||
|
valid.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
} else {
|
||||||
|
valid.value = false
|
||||||
|
errorMessage.value = 'Captcha does not match.'
|
||||||
|
}
|
||||||
|
emit('update:valid', valid.value)
|
||||||
|
emit('validated', valid.value)
|
||||||
|
return valid.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// expose a refresh method to parent via ref
|
||||||
|
defineExpose({ refresh, validate, isValid: computed(() => valid.value) })
|
||||||
|
|
||||||
|
// generate on mount
|
||||||
|
onMounted(() => refresh())
|
||||||
|
|
||||||
|
// // re-validate whenever input changes (lightweight)
|
||||||
|
// watch(input, () => {
|
||||||
|
// // we don't auto-pass until the user explicitly validate (but we can optionally live-validate)
|
||||||
|
// // Here we perform live feedback but still emit validated only when called
|
||||||
|
// const left = normalizeForCompare(input.value)
|
||||||
|
// const right = normalizeForCompare(raw.value)
|
||||||
|
// valid.value = !!input.value && left === right
|
||||||
|
// // emit a live update so the parent can disable submit accordingly
|
||||||
|
// emit('update:valid', valid.value)
|
||||||
|
// })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-2 w-full max-w-sm">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<!-- Captcha visual box -->
|
||||||
|
<div
|
||||||
|
role="img"
|
||||||
|
aria-label="Text captcha, type the characters shown"
|
||||||
|
tabindex="0"
|
||||||
|
class="select-none p-3 rounded-md border border-gray-200 text-white text-xl font-mono tracking-wider text-center w-full"
|
||||||
|
>
|
||||||
|
<span class="inline-block" v-html="display"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Refresh -->
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<Button variant="ghost" type="button" @click="refresh" title="Refresh captcha">
|
||||||
|
<Icon name="i-lucide-refresh-cw" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input -->
|
||||||
|
<div class="flex gap-3 items-start">
|
||||||
|
<div class="flex-grow">
|
||||||
|
<Input
|
||||||
|
v-model="input"
|
||||||
|
:aria-invalid="valid ? 'false' : 'true'"
|
||||||
|
inputmode="text"
|
||||||
|
placeholder="Type the captcha text"
|
||||||
|
@keyup.enter="validate"
|
||||||
|
/>
|
||||||
|
<p v-if="errorMessage" class="text-xs text-red-500 mt-1">{{ errorMessage }}</p>
|
||||||
|
<p v-else-if="valid" class="text-xs text-green-500 mt-1">Correct</p>
|
||||||
|
<p v-else class="text-xs text-gray-500 mt-1">Not case-sensitive</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" type="button" @click="validate" title="Validate"
|
||||||
|
class="border-orange-400">
|
||||||
|
<Icon name="i-lucide-check" class="text-orange-400" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* small nicety: make noise/spaced display look irregular */
|
||||||
|
div[role="img"] {
|
||||||
|
background: url('~/assets/svg/wavey-fingerprint.svg') repeat center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[role="img"] span {
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
font-weight: 600;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
link: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
// submit: [values: InstallationFormData, resetForm: () => void]
|
||||||
|
// cancel: [resetForm: () => void]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Form cancel handler
|
||||||
|
// function onCancelForm({ resetForm }: { resetForm: () => void }) {
|
||||||
|
// emit('cancel', resetForm)
|
||||||
|
// }
|
||||||
|
function onExternalLink() {
|
||||||
|
navigateTo(props.link, {external: true,open: { target: "_blank" },});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button variant="link" class="absolute top-4 right-16" @click="onExternalLink">
|
||||||
|
Open in Browser
|
||||||
|
<Icon name="i-lucide-external-link" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div class="border border-gray-400 rounded-lg w-full h-[80vh] overflow-hidden">
|
||||||
|
<embed style="width: 100%; height: 100%" :src="props.link" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -30,8 +30,8 @@ function onClick(type: ClickType) {
|
|||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-show="props.enableDraft">
|
||||||
<Button v-show="enableDraft" variant="secondary" type="button" @click="onClick('draft')">
|
<Button variant="secondary" type="button" @click="onClick('draft')">
|
||||||
<Icon name="i-lucide-file" />
|
<Icon name="i-lucide-file" />
|
||||||
Draft
|
Draft
|
||||||
</Button>
|
</Button>
|
||||||
@@ -43,4 +43,4 @@ function onClick(type: ClickType) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -19,7 +19,17 @@ export function useQueryCRUDMode(key: string = 'mode') {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const goToEntry = () => (mode.value = 'entry')
|
const goToEntry = () => (mode.value = 'entry')
|
||||||
const backToList = () =>(mode.value = 'list')
|
const backToList = () => {
|
||||||
|
router.push({
|
||||||
|
path: route.path,
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
mode: 'list',
|
||||||
|
// HAPUS record-id
|
||||||
|
'record-id': undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return { mode, goToEntry, backToList }
|
return { mode, goToEntry, backToList }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import type { Permission, RoleAccess } from '~/models/role'
|
import type { Permission, RoleAccess } from '~/models/role'
|
||||||
|
|
||||||
|
export interface PageOperationPermission {
|
||||||
|
canRead: boolean
|
||||||
|
canCreate: boolean
|
||||||
|
canUpdate: boolean
|
||||||
|
canDelete: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user has access to a page
|
* Check if user has access to a page
|
||||||
*/
|
*/
|
||||||
@@ -36,6 +44,13 @@ export function useRBAC() {
|
|||||||
const hasUpdateAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'U')
|
const hasUpdateAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'U')
|
||||||
const hasDeleteAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'D')
|
const hasDeleteAccess = (roleAccess: RoleAccess) => checkPermission(roleAccess, 'D')
|
||||||
|
|
||||||
|
const getPagePermissions = (roleAccess: RoleAccess): PageOperationPermission => ({
|
||||||
|
canRead : hasReadAccess(roleAccess),
|
||||||
|
canCreate: hasCreateAccess(roleAccess),
|
||||||
|
canUpdate: hasUpdateAccess(roleAccess),
|
||||||
|
canDelete: hasDeleteAccess(roleAccess),
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
checkRole,
|
checkRole,
|
||||||
checkPermission,
|
checkPermission,
|
||||||
@@ -44,5 +59,7 @@ export function useRBAC() {
|
|||||||
hasReadAccess,
|
hasReadAccess,
|
||||||
hasUpdateAccess,
|
hasUpdateAccess,
|
||||||
hasDeleteAccess,
|
hasDeleteAccess,
|
||||||
|
getPagePermissions,
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Handlers
|
||||||
|
import { genCrudHandler } from '~/handlers/_handler'
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { create, update, remove } from '~/services/general-consent.service'
|
||||||
|
|
||||||
|
export const {
|
||||||
|
recId,
|
||||||
|
recAction,
|
||||||
|
recItem,
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleActionEdit,
|
||||||
|
handleActionRemove,
|
||||||
|
handleCancelForm,
|
||||||
|
} = genCrudHandler({
|
||||||
|
create,
|
||||||
|
update,
|
||||||
|
remove,
|
||||||
|
})
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { createCrudHandler } from '~/handlers/_handler'
|
||||||
|
import { create, update, remove } from '~/services/medicine-form.service'
|
||||||
|
|
||||||
|
export const {
|
||||||
|
recId,
|
||||||
|
recAction,
|
||||||
|
recItem,
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleActionEdit,
|
||||||
|
handleActionRemove,
|
||||||
|
handleCancelForm,
|
||||||
|
} = createCrudHandler({
|
||||||
|
post: create,
|
||||||
|
patch: update,
|
||||||
|
remove: remove,
|
||||||
|
})
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Handlers
|
||||||
|
import { genCrudHandler } from '~/handlers/_handler'
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { create, update, remove } from '~/services/supporting-document.service'
|
||||||
|
|
||||||
|
export const {
|
||||||
|
recId,
|
||||||
|
recAction,
|
||||||
|
recItem,
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleActionEdit,
|
||||||
|
handleActionRemove,
|
||||||
|
handleCancelForm,
|
||||||
|
} = genCrudHandler({
|
||||||
|
create,
|
||||||
|
update,
|
||||||
|
remove,
|
||||||
|
})
|
||||||
@@ -383,3 +383,45 @@ export const medicalActionTypeCode: Record<string, string> = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type medicalActionTypeCodeKey = keyof typeof medicalActionTypeCode
|
export type medicalActionTypeCodeKey = keyof typeof medicalActionTypeCode
|
||||||
|
|
||||||
|
export const encounterDocTypeCode: Record<string, string> = {
|
||||||
|
"person-resident-number": 'person-resident-number',
|
||||||
|
"person-driving-license": 'person-driving-license',
|
||||||
|
"person-passport": 'person-passport',
|
||||||
|
"person-family-card": 'person-family-card',
|
||||||
|
"mcu-item-result": 'mcu-item-result',
|
||||||
|
"vclaim-sep": 'vclaim-sep',
|
||||||
|
"vclaim-sipp": 'vclaim-sipp',
|
||||||
|
} as const
|
||||||
|
export type encounterDocTypeCodeKey = keyof typeof encounterDocTypeCode
|
||||||
|
export const encounterDocOpt: { label: string; value: encounterDocTypeCodeKey }[] = [
|
||||||
|
{ label: 'KTP', value: 'person-resident-number' },
|
||||||
|
{ label: 'SIM', value: 'person-driving-license' },
|
||||||
|
{ label: 'Passport', value: 'person-passport' },
|
||||||
|
{ label: 'Kartu Keluarga', value: 'person-family-card' },
|
||||||
|
{ label: 'Hasil MCU', value: 'mcu-item-result' },
|
||||||
|
{ label: 'Klaim SEP', value: 'vclaim-sep' },
|
||||||
|
{ label: 'Klaim SIPP', value: 'vclaim-sipp' },
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
export const docTypeCode = {
|
||||||
|
"encounter-patient": 'encounter-patient',
|
||||||
|
"encounter-support": 'encounter-support',
|
||||||
|
"encounter-other": 'encounter-other',
|
||||||
|
"vclaim-sep": 'vclaim-sep',
|
||||||
|
"vclaim-sipp": 'vclaim-sipp',
|
||||||
|
} as const
|
||||||
|
export const docTypeLabel = {
|
||||||
|
"encounter-patient": 'Data Pasien',
|
||||||
|
"encounter-support": 'Data Penunjang',
|
||||||
|
"encounter-other": 'Lain - Lain',
|
||||||
|
"vclaim-sep": 'SEP',
|
||||||
|
"vclaim-sipp": 'SIPP',
|
||||||
|
} as const
|
||||||
|
export type docTypeCodeKey = keyof typeof docTypeCode
|
||||||
|
export const supportingDocOpt = [
|
||||||
|
{ label: 'Data Pasien', value: 'encounter-patient' },
|
||||||
|
{ label: 'Data Penunjang', value: 'encounter-support' },
|
||||||
|
{ label: 'Lain - Lain', value: 'encounter-other' },
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ClassValue } from 'clsx'
|
import type { ClassValue } from 'clsx'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
|
||||||
export interface SelectOptionType<_T = string> {
|
export interface SelectOptionType<_T = string> {
|
||||||
value: string
|
value: string
|
||||||
@@ -104,3 +105,59 @@ export function calculateAge(birthDate: Date | string | null | undefined): strin
|
|||||||
return `${years} tahun ${months} bulan`
|
return `${years} tahun ${months} bulan`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a plain JavaScript object (including File objects) into a FormData instance.
|
||||||
|
* @param {object} data - The object to convert (e.g., form values).
|
||||||
|
* @returns {FormData} The new FormData object suitable for API submission.
|
||||||
|
*/
|
||||||
|
export function toFormData(data: Record<string, any>): FormData {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||||
|
const value = data[key];
|
||||||
|
|
||||||
|
// Handle File objects, Blobs, or standard JSON values
|
||||||
|
if (value !== null && value !== undefined) {
|
||||||
|
// Check if the value is a File/Blob instance
|
||||||
|
if (value instanceof File || value instanceof Blob) {
|
||||||
|
// Append the file directly
|
||||||
|
formData.append(key, value);
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
// Handle nested objects/arrays by stringifying them (optional, depends on API)
|
||||||
|
// Note: Most APIs expect nested data to be handled separately or passed as JSON string
|
||||||
|
// For simplicity, we stringify non-File objects.
|
||||||
|
formData.append(key, JSON.stringify(value));
|
||||||
|
} else {
|
||||||
|
// Append standard string, number, or boolean values
|
||||||
|
formData.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printFormData(formData: FormData) {
|
||||||
|
console.log("--- FormData Contents ---");
|
||||||
|
// Use the entries() iterator to loop through key/value pairs
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
if (value instanceof File) {
|
||||||
|
console.log(`Key: ${key}, Value: [File: ${value.name}, Type: ${value.type}, Size: ${value.size} bytes]`);
|
||||||
|
} else {
|
||||||
|
console.log(`Key: ${key}, Value: "${value}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("-------------------------");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unauthorizedToast() {
|
||||||
|
toast({
|
||||||
|
title: 'Unauthorized',
|
||||||
|
description: 'You are not authorized to perform this action.',
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { type Base, genBase } from "./_base"
|
||||||
|
import { docTypeLabel, } from '~/lib/constants'
|
||||||
|
import { genEmployee, type Employee } from "./employee"
|
||||||
|
import { genEncounter, type Encounter } from "./encounter"
|
||||||
|
|
||||||
|
export interface EncounterDocument extends Base {
|
||||||
|
encounter_id: number
|
||||||
|
encounter?: Encounter
|
||||||
|
upload_employee_id: number
|
||||||
|
employee?: Employee
|
||||||
|
type_code: string
|
||||||
|
name: string
|
||||||
|
filePath: string
|
||||||
|
fileName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genEncounterDocument(): EncounterDocument {
|
||||||
|
return {
|
||||||
|
...genBase(),
|
||||||
|
encounter_id: 2,
|
||||||
|
encounter: genEncounter(),
|
||||||
|
upload_employee_id: 0,
|
||||||
|
employee: genEmployee(),
|
||||||
|
type_code: docTypeLabel["encounter-patient"],
|
||||||
|
name: 'example',
|
||||||
|
filePath: 'https://bing.com',
|
||||||
|
fileName: 'example',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { DeathCause } from "./death-cause"
|
import type { DeathCause } from "./death-cause"
|
||||||
import { type Doctor, genDoctor } from "./doctor"
|
import { type Doctor, genDoctor } from "./doctor"
|
||||||
import { genEmployee, type Employee } from "./employee"
|
import { genEmployee, type Employee } from "./employee"
|
||||||
|
import type { EncounterDocument } from "./encounter-document"
|
||||||
import type { InternalReference } from "./internal-reference"
|
import type { InternalReference } from "./internal-reference"
|
||||||
import { type Patient, genPatient } from "./patient"
|
import { type Patient, genPatient } from "./patient"
|
||||||
import type { Specialist } from "./specialist"
|
import type { Specialist } from "./specialist"
|
||||||
@@ -37,6 +38,7 @@ export interface Encounter {
|
|||||||
internalReferences?: InternalReference[]
|
internalReferences?: InternalReference[]
|
||||||
deathCause?: DeathCause
|
deathCause?: DeathCause
|
||||||
status_code: string
|
status_code: string
|
||||||
|
encounterDocuments: EncounterDocument[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function genEncounter(): Encounter {
|
export function genEncounter(): Encounter {
|
||||||
@@ -54,7 +56,8 @@ export function genEncounter(): Encounter {
|
|||||||
appointment_doctor_id: 0,
|
appointment_doctor_id: 0,
|
||||||
appointment_doctor: genDoctor(),
|
appointment_doctor: genDoctor(),
|
||||||
medicalDischargeEducation: '',
|
medicalDischargeEducation: '',
|
||||||
status_code: ''
|
status_code: '',
|
||||||
|
encounterDocuments: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
export interface GeneralConsent {
|
||||||
|
id: number
|
||||||
|
encounter_id: number
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValueCreateDto {
|
||||||
|
relatives: string[]
|
||||||
|
responsibleName: string
|
||||||
|
responsiblePhone: string
|
||||||
|
informant: string
|
||||||
|
witness1: string
|
||||||
|
witness2: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDto {
|
||||||
|
encounter_id: number
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDto {
|
||||||
|
id: number
|
||||||
|
problem: string
|
||||||
|
unit_id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteDto {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genCreateDto(): CreateDto {
|
||||||
|
return {
|
||||||
|
encounter_id: 0,
|
||||||
|
problem: '',
|
||||||
|
unit_id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genConsultation(): GeneralConsent {
|
||||||
|
return {
|
||||||
|
id: 0,
|
||||||
|
encounter_id: 0,
|
||||||
|
unit_id: 0,
|
||||||
|
doctor_id: 0,
|
||||||
|
problem: '',
|
||||||
|
solution: '',
|
||||||
|
repliedAt: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { type Base, genBase } from "./_base"
|
||||||
|
|
||||||
|
export interface MedicineForm extends Base {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDto {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetListDto {
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
name?: string
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDetailDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDto extends CreateDto {
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genMedicine(): MedicineForm {
|
||||||
|
return {
|
||||||
|
...genBase(),
|
||||||
|
name: 'name',
|
||||||
|
code: 'code',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,9 @@ useHead({
|
|||||||
title: () => route.meta.title as string,
|
title: () => route.meta.title as string,
|
||||||
})
|
})
|
||||||
|
|
||||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
|
||||||
|
|
||||||
const { checkRole, hasReadAccess } = useRBAC()
|
const { checkRole, getPagePermissions } = useRBAC()
|
||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
@@ -27,14 +27,13 @@ const hasAccess = checkRole(roleAccess)
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
// const canRead = hasReadAccess(roleAccess)
|
const pagePermission = getPagePermissions(roleAccess)
|
||||||
const canRead = true
|
|
||||||
const callbackUrl = route.query['return-path'] as string | undefined
|
const callbackUrl = route.query['return-path'] as string | undefined
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="canRead">
|
<div v-if="pagePermission.canRead">
|
||||||
<ContentControlLetterAdd :callback-url="callbackUrl" />
|
<ContentControlLetterAdd :callback-url="callbackUrl" />
|
||||||
</div>
|
</div>
|
||||||
<Error v-else :status-code="403" />
|
<Error v-else :status-code="403" />
|
||||||
|
|||||||
+10
-16
@@ -2,46 +2,40 @@
|
|||||||
import type { PagePermission } from '~/models/role'
|
import type { PagePermission } from '~/models/role'
|
||||||
import Error from '~/components/pub/my-ui/error/error.vue'
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||||
import ContentChemotherapyAdminList from '~/components/content/chemotherapy/admin-list.vue'
|
|
||||||
import ContentChemotherapyVerification from '~/components/content/chemotherapy/verification.vue'
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
middleware: ['rbac'],
|
||||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||||
title: 'Kemoterapi Admin',
|
title: 'Update Dokumen Pendukung',
|
||||||
contentFrame: 'cf-full-width',
|
contentFrame: 'cf-full-width',
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: () => 'Verifikasi Jadwal Pasien',
|
title: () => route.meta.title as string,
|
||||||
})
|
})
|
||||||
|
|
||||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/doctor'] || {}
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||||
|
|
||||||
const { checkRole, hasReadAccess } = useRBAC()
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
if (!hasAccess) {
|
// if (!hasAccess) {
|
||||||
navigateTo('/403')
|
// navigateTo('/403')
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canRead = true // hasReadAccess(roleAccess)
|
// const canRead = hasReadAccess(roleAccess)
|
||||||
|
const canRead = true
|
||||||
const mode = computed(() => route.params.mode as string)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="canRead">
|
<div v-if="canRead">
|
||||||
<ContentChemotherapyVerification />
|
<ContentDocumentUploadEdit/>
|
||||||
</div>
|
</div>
|
||||||
<Error
|
<Error v-else :status-code="403" />
|
||||||
v-else
|
|
||||||
:status-code="403"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PagePermission } from '~/models/role'
|
||||||
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
|
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['rbac'],
|
||||||
|
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||||
|
title: 'Tambah Dokumen Pendukung',
|
||||||
|
contentFrame: 'cf-full-width',
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: () => route.meta.title as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||||
|
|
||||||
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
|
// Check if user has access to this page
|
||||||
|
const hasAccess = checkRole(roleAccess)
|
||||||
|
// if (!hasAccess) {
|
||||||
|
// navigateTo('/403')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Define permission-based computed properties
|
||||||
|
// const canRead = hasReadAccess(roleAccess)
|
||||||
|
const canRead = true
|
||||||
|
const callbackUrl = route.query['return-path'] as string | undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="canRead">
|
||||||
|
<ContentDocumentUploadAdd :callback-url="callbackUrl" />
|
||||||
|
</div>
|
||||||
|
<Error v-else :status-code="403" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -18,7 +18,7 @@ useHead({
|
|||||||
|
|
||||||
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
|
||||||
|
|
||||||
const { checkRole, hasCreateAccess } = useRBAC()
|
const { checkRole, hasCreateAccess, getPagePermissions } = useRBAC()
|
||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
@@ -30,11 +30,11 @@ const hasAccess = checkRole(roleAccess)
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canCreate = true // hasCreateAccess(roleAccess)
|
const pagePermission = getPagePermissions(roleAccess)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="canCreate">
|
<div v-if="pagePermission.canRead">
|
||||||
<ContentEncounterProcess />
|
<ContentEncounterProcess />
|
||||||
</div>
|
</div>
|
||||||
<Error v-else :status-code="403" />
|
<Error v-else :status-code="403" />
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ const { checkRole, hasReadAccess } = useRBAC()
|
|||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
if (!hasAccess) {
|
// if (!hasAccess) {
|
||||||
// navigateTo('/403')
|
// navigateTo('/403')
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canRead = true // hasReadAccess(roleAccess)
|
const canRead = true // hasReadAccess(roleAccess)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PagePermission } from '~/models/role'
|
||||||
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
|
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['rbac'],
|
||||||
|
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||||
|
title: 'Resume',
|
||||||
|
contentFrame: 'cf-full-width',
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: () => `${route.meta.title}`, // backtick to avoid the ts-plugin(2322) warning
|
||||||
|
})
|
||||||
|
|
||||||
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
|
||||||
|
|
||||||
|
const { checkRole, hasCreateAccess } = useRBAC()
|
||||||
|
|
||||||
|
// Check if user has access to this page
|
||||||
|
const hasAccess = checkRole(roleAccess)
|
||||||
|
// if (!hasAccess) {
|
||||||
|
// throw createError({
|
||||||
|
// statusCode: 403,
|
||||||
|
// statusMessage: 'Access denied',
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Define permission-based computed properties
|
||||||
|
const canCreate = true // hasCreateAccess(roleAccess)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="canCreate">
|
||||||
|
<ContentResumeAdd />
|
||||||
|
</div>
|
||||||
|
<Error v-else :status-code="403" />
|
||||||
|
</template>
|
||||||
+9
-11
@@ -6,8 +6,8 @@ import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
|||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['rbac'],
|
middleware: ['rbac'],
|
||||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||||
title: 'Daftar Kempterapi',
|
title: 'Daftar Dokter',
|
||||||
contentFrame: 'cf-full-width',
|
contentFrame: 'cf-container-lg',
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -22,19 +22,17 @@ const { checkRole, hasReadAccess } = useRBAC()
|
|||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
if (!hasAccess) {
|
// if (!hasAccess) {
|
||||||
navigateTo('/403')
|
// navigateTo('/403')
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canRead = true // hasReadAccess(roleAccess)
|
const canRead = true // hasReadAccess(roleAccess)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<template v-if="canRead">
|
||||||
<div v-if="canRead">
|
<ContentMedicineFormList />
|
||||||
<ContentChemotherapyList />
|
</template>
|
||||||
</div>
|
<Error v-else :status-code="403" />
|
||||||
<Error v-else :status-code="403" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
@@ -22,12 +22,12 @@ const { checkRole, hasReadAccess } = useRBAC()
|
|||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
if (!hasAccess) {
|
// if (!hasAccess) {
|
||||||
navigateTo('/403')
|
// navigateTo('/403')
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canRead = hasReadAccess(roleAccess)
|
const canRead = true // hasReadAccess(roleAccess)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface GenerateFile {
|
||||||
|
entityType_code: string
|
||||||
|
ref_id: number
|
||||||
|
type_code: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const ACCEPTED_UPLOAD_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
|
||||||
|
const MAX_SIZE_BYTES = 1 * 1024 * 1024 // 1MB
|
||||||
|
|
||||||
|
const DocumentUploadSchema = z.object({
|
||||||
|
entityType_code: z.string().default('encounter'),
|
||||||
|
ref_id: z.number(),
|
||||||
|
upload_employee_id: z.number().optional(),
|
||||||
|
// upload_employee_id: z.number(),
|
||||||
|
name: z.string({ required_error: 'Mohon isi', }),
|
||||||
|
type_code: z.string({ required_error: 'Mohon isi', }),
|
||||||
|
content: z.custom<File>()
|
||||||
|
.refine((f) => f, { message: 'File tidak boleh kosong' })
|
||||||
|
.refine((f) => !f || f instanceof File, { message: 'Harus berupa file yang valid' })
|
||||||
|
.refine((f) => !f || ACCEPTED_UPLOAD_TYPES.includes(f.type), {
|
||||||
|
message: 'Format file harus JPG, PNG, atau PDF',
|
||||||
|
})
|
||||||
|
.refine((f) => !f || f.size <= MAX_SIZE_BYTES, { message: 'Maksimal 1MB' }),
|
||||||
|
})
|
||||||
|
|
||||||
|
type DocumentUploadFormData = z.infer<typeof DocumentUploadSchema>
|
||||||
|
|
||||||
|
export { DocumentUploadSchema }
|
||||||
|
export type { DocumentUploadFormData }
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import type { CreateDto } from '~/models/general-consent'
|
||||||
|
|
||||||
|
const GeneralConsentSchema = z.object({
|
||||||
|
relatives: z.array(z.object({ name: z.string(), phone: z.string() })),
|
||||||
|
responsibleName: z.string().optional(),
|
||||||
|
responsiblePhone: z.string().optional(),
|
||||||
|
informant: z.string().optional(),
|
||||||
|
witness1: z.string().optional(),
|
||||||
|
witness2: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type GeneralConsentFormData = z.infer<typeof GeneralConsentSchema> & CreateDto
|
||||||
|
|
||||||
|
export { GeneralConsentSchema }
|
||||||
|
export type { GeneralConsentFormData }
|
||||||
@@ -5,6 +5,7 @@ export const MedicineSchema = z.object({
|
|||||||
name: z.string({ required_error: 'Nama harus diisi' }).min(1, 'Nama minimal 1 karakter'),
|
name: z.string({ required_error: 'Nama harus diisi' }).min(1, 'Nama minimal 1 karakter'),
|
||||||
medicineGroup_code: z.string({ required_error: 'Kelompok obat harus diisi' }).min(1, 'Kelompok obat harus diisi'),
|
medicineGroup_code: z.string({ required_error: 'Kelompok obat harus diisi' }).min(1, 'Kelompok obat harus diisi'),
|
||||||
medicineMethod_code: z.string({ required_error: 'Metode pemberian harus diisi' }).min(1, 'Metode pemberian harus diisi'),
|
medicineMethod_code: z.string({ required_error: 'Metode pemberian harus diisi' }).min(1, 'Metode pemberian harus diisi'),
|
||||||
|
medicineForm_code: z.string({ required_error: 'Sediaan Obat harus diisi' }).min(1, 'Sediaan Obat harus diisi'),
|
||||||
uom_code: z.string({ required_error: 'Satuan harus diisi' }).min(1, 'Satuan harus diisi'),
|
uom_code: z.string({ required_error: 'Satuan harus diisi' }).min(1, 'Satuan harus diisi'),
|
||||||
infra_id: z.number().nullable().optional(),
|
infra_id: z.number().nullable().optional(),
|
||||||
stock: z.preprocess((val) => Number(val), z.number({ invalid_type_error: 'Stok harus berupa angka' }).min(1, 'Stok harus lebih besar dari 0')),
|
stock: z.preprocess((val) => Number(val), z.number({ invalid_type_error: 'Stok harus berupa angka' }).min(1, 'Stok harus lebih besar dari 0')),
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import type { CreateDto } from '~/models/consultation'
|
||||||
|
|
||||||
|
export type ResumeArrangementType = "krs" | "mrs" | "rujukInternal" | "rujukExternal" | "meninggal" | "other"
|
||||||
|
|
||||||
|
const SecondaryDiagnosisSchema = z.object({
|
||||||
|
diagnosis: z.string({ required_error: 'Diagnosis harus diisi' }),
|
||||||
|
icd10: z.string({ required_error: 'ICD 10 harus diisi' }),
|
||||||
|
diagnosisBasis: z.string({ required_error: 'Dasar Diagnosis harus diisi' })
|
||||||
|
.min(1, 'Uraian minimum 1 karakter')
|
||||||
|
.max(2048, 'Uraian maksimum 2048 karakter'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const SecondaryActionSchema = z.object({
|
||||||
|
action: z.string({ required_error: 'Action harus diisi' }),
|
||||||
|
icd9: z.string({ required_error: 'ICD 10 harus diisi' }),
|
||||||
|
actionBasis: z.string({ required_error: 'Dasar Action harus diisi' })
|
||||||
|
.min(1, 'Uraian minimum 1 karakter')
|
||||||
|
.max(2048, 'Uraian maksimum 2048 karakter'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const ConsultationSchema = z.object({
|
||||||
|
consultation: z.string({ required_error: 'Consultation harus diisi' })
|
||||||
|
.min(1, 'Uraian minimum 1 karakter')
|
||||||
|
.max(2048, 'Uraian maksimum 2048 karakter'),
|
||||||
|
consultationReply: z.string({ required_error: 'Jawaban Consultation harus diisi' })
|
||||||
|
.min(1, 'Uraian minimum 1 karakter')
|
||||||
|
.max(2048, 'Uraian maksimum 2048 karakter'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const ResumeSchema = z.object({
|
||||||
|
inDate: z.string({ required_error: 'Tanggal harus diisi' }),
|
||||||
|
outDate: z.string({ required_error: 'Tanggal harus diisi' }),
|
||||||
|
anamnesis: z.number({ required_error: 'Anamnesis harus diisi' })
|
||||||
|
.min(1, 'Anamnesis minimum 1 karakter')
|
||||||
|
.max(2048, 'Anamnesis maksimum 2048 karakter'),
|
||||||
|
physicalCheckup: z.string({ required_error: 'Uraian harus diisi' })
|
||||||
|
.min(1, 'Uraian minimum 1 karakter')
|
||||||
|
.max(2048, 'Uraian maksimum 2048 karakter'),
|
||||||
|
supplementCheckup: z.string({ required_error: 'Uraian harus diisi' })
|
||||||
|
.min(1, 'Uraian minimum 1 karakter')
|
||||||
|
.max(2048, 'Uraian maksimum 2048 karakter'),
|
||||||
|
|
||||||
|
primaryDiagnosis: z.string({ required_error: 'Diagnosis harus diisi' }),
|
||||||
|
secondaryDiagnosis: z.array(SecondaryDiagnosisSchema).optional(),
|
||||||
|
|
||||||
|
primaryOperativeNonOperativeAct: z.string({ required_error: 'Diagnosis harus diisi' }),
|
||||||
|
secondaryOperativeNonOperativeAct: z.array(SecondaryActionSchema).optional(),
|
||||||
|
medikamentosa: z.string({ required_error: 'Uraian harus diisi' })
|
||||||
|
.min(1, 'Uraian minimum 1 karakter')
|
||||||
|
.max(2048, 'Uraian maksimum 2048 karakter'),
|
||||||
|
|
||||||
|
consultation: z.array(ConsultationSchema).optional(),
|
||||||
|
|
||||||
|
arrangement: z.custom<ResumeArrangementType>().default("krs"),
|
||||||
|
inpatientIndication: z.string({ required_error: 'Uraian harus diisi' })
|
||||||
|
.min(1, 'Uraian minimum 1 karakter')
|
||||||
|
.max(2048, 'Uraian maksimum 2048 karakter'),
|
||||||
|
faskes: z.string({ required_error: 'Faskes harus diisi' }).optional(),
|
||||||
|
clinic: z.string({ required_error: 'Klinik harus diisi' }).optional(),
|
||||||
|
deathDate: z.string({ required_error: 'Tanggal harus diisi' }).optional(),
|
||||||
|
deathCause: z.array(z.string()).optional().default([]),
|
||||||
|
deathCauseDescription: z.string({ required_error: 'Uraian harus diisi' })
|
||||||
|
.min(1, 'Uraian minimum 1 karakter')
|
||||||
|
.max(2048, 'Uraian maksimum 2048 karakter')
|
||||||
|
.optional(),
|
||||||
|
keterangan: z.string({ required_error: 'Uraian harus diisi' })
|
||||||
|
.min(1, 'Uraian minimum 1 karakter')
|
||||||
|
.max(2048, 'Uraian maksimum 2048 karakter')
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type ResumeFormData = z.infer<typeof ResumeSchema> & (CreateDto)
|
||||||
|
|
||||||
|
export { ResumeSchema }
|
||||||
|
export type { ResumeFormData }
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const VerificationSchema = z.object({
|
||||||
|
name: z.string({
|
||||||
|
required_error: 'Mohon lengkapi Nama Anda',
|
||||||
|
}),
|
||||||
|
email: z.string({
|
||||||
|
required_error: 'Mohon lengkapi email',
|
||||||
|
}),
|
||||||
|
password: z.string({
|
||||||
|
required_error: 'Mohon lengkapi password',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
type VerificationFormData = z.infer<typeof VerificationSchema>
|
||||||
|
|
||||||
|
export { VerificationSchema, }
|
||||||
|
export type { VerificationFormData, }
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import * as base from './_crud-base'
|
||||||
|
|
||||||
|
const path = '/api/v1/general-consent'
|
||||||
|
|
||||||
|
export function create(data: any) {
|
||||||
|
return base.create(path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getList(params: any = null) {
|
||||||
|
return base.getList(path, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDetail(id: number | string) {
|
||||||
|
return base.getDetail(path, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function update(id: number | string, data: any) {
|
||||||
|
return base.update(path, id, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remove(id: number | string) {
|
||||||
|
return base.remove(path, id)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import * as base from './_crud-base'
|
||||||
|
|
||||||
|
const path = '/api/v1/generate-file'
|
||||||
|
|
||||||
|
export function create(data: any) {
|
||||||
|
return base.create(path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getList(params: any = null) {
|
||||||
|
return base.getList(path, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDetail(id: number | string) {
|
||||||
|
return base.getDetail(path, id)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Base
|
||||||
|
import * as base from './_crud-base'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { MedicineForm } from '~/models/medicine-form'
|
||||||
|
|
||||||
|
const path = '/api/v1/medicine-form'
|
||||||
|
const name = 'medicine-form'
|
||||||
|
|
||||||
|
export function create(data: any) {
|
||||||
|
return base.create(path, data, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getList(params: any = null) {
|
||||||
|
return base.getList(path, params, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDetail(id: number | string) {
|
||||||
|
return base.getDetail(path, id, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function update(id: number | string, data: any) {
|
||||||
|
return base.update(path, id, data, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remove(id: number | string) {
|
||||||
|
return base.remove(path, id, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
|
||||||
|
let data: { value: string; label: string }[] = []
|
||||||
|
const result = await getList(params)
|
||||||
|
if (result.success) {
|
||||||
|
const resultData = result.body?.data || []
|
||||||
|
data = resultData.map((item: MedicineForm) => ({
|
||||||
|
value: item.code,
|
||||||
|
label: item.name,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// Base
|
||||||
|
import * as base from './_crud-base'
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
import { uploadCode, type UploadCodeKey } from '~/lib/constants'
|
||||||
|
|
||||||
|
const path = '/api/v1/encounter-document'
|
||||||
|
const create_path = '/api/v1/upload-file'
|
||||||
|
const name = 'encounter-document'
|
||||||
|
|
||||||
|
export function create(data: any) {
|
||||||
|
return base.create(create_path, data, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getList(params: any = null) {
|
||||||
|
return base.getList(path, params, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDetail(id: number | string, params?: any) {
|
||||||
|
return base.getDetail(path, id, name, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function update(id: number | string, data: any) {
|
||||||
|
return base.update(path, id, data, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remove(id: number | string) {
|
||||||
|
return base.remove(path, id, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadAttachment(file: File, userId: number, key: UploadCodeKey) {
|
||||||
|
try {
|
||||||
|
const resolvedKey = uploadCode[key]
|
||||||
|
if (!resolvedKey) {
|
||||||
|
throw new Error(`Invalid upload code key: ${key}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// siapkan form-data body
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('code', resolvedKey)
|
||||||
|
formData.append('content', file)
|
||||||
|
|
||||||
|
// kirim via xfetch
|
||||||
|
const resp = await xfetch(`${path}/${userId}/upload`, 'POST', formData)
|
||||||
|
|
||||||
|
// struktur hasil sama seperti patchPatient
|
||||||
|
const result: any = {}
|
||||||
|
result.success = resp.success
|
||||||
|
result.body = (resp.body as Record<string, any>) || {}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading attachment:', error)
|
||||||
|
throw new Error('Failed to upload attachment')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"heading": "Menu Utama",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Dashboard",
|
||||||
|
"icon": "i-lucide-home",
|
||||||
|
"link": "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Rawat Jalan",
|
||||||
|
"icon": "i-lucide-stethoscope",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Antrian Pendaftaran",
|
||||||
|
"link": "/outpatient/registration-queue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Antrian Poliklinik",
|
||||||
|
"link": "/outpatient/polyclinic-queue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Kunjungan",
|
||||||
|
"link": "/outpatient/encounter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Konsultasi",
|
||||||
|
"link": "/outpatient/consultation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "IGD",
|
||||||
|
"icon": "i-lucide-zap",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Triase",
|
||||||
|
"link": "/emergency/triage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Kunjungan",
|
||||||
|
"link": "/emergency/encounter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Konsultasi",
|
||||||
|
"link": "/emergency/consultation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Rehab Medik",
|
||||||
|
"icon": "i-lucide-bike",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Antrean Pendaftaran",
|
||||||
|
"link": "/rehab/registration-queue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Antrean Poliklinik",
|
||||||
|
"link": "/rehab/polyclinic-queue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Kunjungan",
|
||||||
|
"link": "/rehab/encounter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Konsultasi",
|
||||||
|
"link": "/rehab/consultation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Rawat Inap",
|
||||||
|
"icon": "i-lucide-building-2",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Permintaan",
|
||||||
|
"link": "/inpatient/request"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Kunjungan",
|
||||||
|
"link": "/inpatient/encounter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Konsultasi",
|
||||||
|
"link": "/inpatient/consultation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Obat - Order",
|
||||||
|
"icon": "i-lucide-briefcase-medical",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Permintaan",
|
||||||
|
"link": "/medication/order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Standing Order",
|
||||||
|
"link": "/medication/standing-order"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Lab - Order",
|
||||||
|
"icon": "i-lucide-microscope",
|
||||||
|
"link": "/pc-lab-order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Lab Mikro - Order",
|
||||||
|
"icon": "i-lucide-microscope",
|
||||||
|
"link": "/micro-lab-order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Lab PA - Order",
|
||||||
|
"icon": "i-lucide-microscope",
|
||||||
|
"link": "/pa-lab-order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Radiologi - Order",
|
||||||
|
"icon": "i-lucide-radio",
|
||||||
|
"link": "/radiology-order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Gizi",
|
||||||
|
"icon": "i-lucide-egg-fried",
|
||||||
|
"link": "/nutrition-order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Pembayaran",
|
||||||
|
"icon": "i-lucide-banknote-arrow-up",
|
||||||
|
"link": "/payment"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Ruang Tindakan Rajal",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Kemoterapi",
|
||||||
|
"icon": "i-lucide-droplets",
|
||||||
|
"link": "/outpation-action/cemotherapy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Hemofilia",
|
||||||
|
"icon": "i-lucide-droplet-off",
|
||||||
|
"link": "/outpation-action/hemophilia"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Ruang Tindakan Anak",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Thalasemi",
|
||||||
|
"icon": "i-lucide-baby",
|
||||||
|
"link": "/children-action/thalasemia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Echocardiography",
|
||||||
|
"icon": "i-lucide-baby",
|
||||||
|
"link": "/children-action/echocardiography"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Spirometri",
|
||||||
|
"icon": "i-lucide-baby",
|
||||||
|
"link": "/children-action/spirometry"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Client",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Pasien",
|
||||||
|
"icon": "i-lucide-users",
|
||||||
|
"link": "/client/patient"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Rekam Medis",
|
||||||
|
"icon": "i-lucide-file-text",
|
||||||
|
"link": "/client/medical-record"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Integrasi",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "BPJS",
|
||||||
|
"icon": "i-lucide-circuit-board",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "SEP",
|
||||||
|
"icon": "i-lucide-circuit-board",
|
||||||
|
"link": "/integration/bpjs/sep"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Peserta",
|
||||||
|
"icon": "i-lucide-circuit-board",
|
||||||
|
"link": "/integration/bpjs/member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Surat Kontrol",
|
||||||
|
"icon": "i-lucide-circuit-board",
|
||||||
|
"link": "/integration/bpjs/control-letter"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "SATUSEHAT",
|
||||||
|
"icon": "i-lucide-database",
|
||||||
|
"link": "/integration/satusehat"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Source",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Peralatan dan Perlengkapan",
|
||||||
|
"icon": "i-lucide-layout-dashboard",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Obat",
|
||||||
|
"link": "/tools-equipment-src/medicine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Peralatan",
|
||||||
|
"link": "/tools-equipment-src/tools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Perlengkapan (BMHP)",
|
||||||
|
"link": "/tools-equipment-src/equipment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Metode Obat",
|
||||||
|
"link": "/tools-equipment-src/medicine-method"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Jenis Obat",
|
||||||
|
"link": "/tools-equipment-src/medicine-type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Sediaan Obat",
|
||||||
|
"link": "/tools-equipment-src/medicine-form"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Pengguna",
|
||||||
|
"icon": "i-lucide-user",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Pegawai",
|
||||||
|
"link": "/human-src/employee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "PPDS",
|
||||||
|
"link": "/human-src/specialist-intern"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Pemeriksaan Penunjang",
|
||||||
|
"icon": "i-lucide-layout-list",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Checkup",
|
||||||
|
"link": "/mcu-src/mcu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Prosedur",
|
||||||
|
"link": "/mcu-src/procedure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Diagnosis",
|
||||||
|
"link": "/mcu-src/diagnose"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Medical Action",
|
||||||
|
"link": "/mcu-src/medical-action"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Layanan",
|
||||||
|
"icon": "i-lucide-layout-list",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Counter",
|
||||||
|
"link": "/service-src/counter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Public Screen (Big Screen)",
|
||||||
|
"link": "/service-src/public-screen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Kasur",
|
||||||
|
"link": "/service-src/bed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Kamar",
|
||||||
|
"link": "/service-src/chamber"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Ruang",
|
||||||
|
"link": "/service-src/room"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Depo",
|
||||||
|
"link": "/service-src/warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Lantai",
|
||||||
|
"link": "/service-src/floor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Gedung",
|
||||||
|
"link": "/service-src/building"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Organisasi",
|
||||||
|
"icon": "i-lucide-network",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Divisi",
|
||||||
|
"link": "/org-src/division"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Instalasi",
|
||||||
|
"link": "/org-src/installation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Unit",
|
||||||
|
"link": "/org-src/unit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Spesialis",
|
||||||
|
"link": "/org-src/specialist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Sub Spesialis",
|
||||||
|
"link": "/org-src/subspecialist"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Umum",
|
||||||
|
"icon": "i-lucide-airplay",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Uom",
|
||||||
|
"link": "/common/uom"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Keuangan",
|
||||||
|
"icon": "i-lucide-airplay",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Item & Pricing",
|
||||||
|
"link": "/common/item"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -240,6 +240,10 @@
|
|||||||
{
|
{
|
||||||
"title": "Jenis Obat",
|
"title": "Jenis Obat",
|
||||||
"link": "/tools-equipment-src/medicine-type"
|
"link": "/tools-equipment-src/medicine-type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Sediaan Obat",
|
||||||
|
"link": "/tools-equipment-src/medicine-form"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user