Merge branch 'dev' into feat/medicine-form-167

This commit is contained in:
2025-11-21 08:17:43 +07:00
66 changed files with 4473 additions and 37 deletions
+1
View File
@@ -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,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,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>
+482
View File
@@ -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 '-'
// },
},
}
+101
View File
@@ -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 '-'
},
},
}
+31
View File
@@ -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>
+9 -5
View File
@@ -20,6 +20,8 @@ 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 DocUploadList from '~/components/content/document-upload/list.vue'
import { genEncounter } from '~/models/encounter' import { genEncounter } from '~/models/encounter'
import GeneralConsentList from '~/components/content/general-consent/entry.vue'
import ResumeList from '~/components/content/resume/list.vue'
import ControlLetterList from '~/components/content/control-letter/list.vue' import ControlLetterList from '~/components/content/control-letter/list.vue'
const route = useRoute() const route = useRoute()
@@ -69,21 +71,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', label: 'Order Alkes' }, { value: 'device', label: 'Order Alkes' },
{ 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', component: DocUploadList, props: { encounter: data, }, }, { value: 'supporting-document', label: 'Upload Dokumen Pendukung', component: DocUploadList, props: { encounter: data }, },
] ]
</script> </script>
@@ -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>
+200
View File
@@ -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>
+215
View File
@@ -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>
@@ -8,6 +8,8 @@ interface ConfirmationProps {
message?: string message?: string
confirmText?: string confirmText?: string
cancelText?: string cancelText?: string
noTrueSlot?: boolean
skipClosingMessage?: boolean
variant?: 'default' | 'destructive' | 'warning' variant?: 'default' | 'destructive' | 'warning'
size?: 'sm' | 'md' | 'lg' | 'xl' size?: 'sm' | 'md' | 'lg' | 'xl'
} }
@@ -71,20 +73,22 @@ function handleCancel() {
<Dialog v-model:open="isOpen" :title="title" :size="size"> <Dialog v-model:open="isOpen" :title="title" :size="size">
<div class="space-y-4"> <div class="space-y-4">
<!-- Icon dan pesan --> <!-- Icon dan pesan -->
<div class="flex items-center gap-3"> <div class="flex items-start gap-3">
<div :class="[variantClasses.icon, variantClasses.iconColor]" class="w-6 h-6 mt-1 flex-shrink-0" /> <div :class="[variantClasses.icon, variantClasses.iconColor]" class="w-4 h-4 2xl:h-5 2xl:h-6 flex-shrink-0" />
<div class="flex-1"> <div class="flex-1">
<p class="text-sm text-muted-foreground leading-relaxed"> {{ message }} {{ !noTrueSlot ? ' dengan informasi sebagai berikut:' : '.' }}
{{ message }}
</p>
</div> </div>
</div> </div>
<!-- Slot untuk konten custom --> <!-- Slot untuk konten custom -->
<div v-if="$slots.default"> <div>
<slot /> <slot />
</div> </div>
<div v-if="!skipClosingMessage" class="">
Lanjutkan Proses?
</div>
<!-- Footer buttons --> <!-- Footer buttons -->
<div class="flex justify-end gap-3 pt-4"> <div class="flex justify-end gap-3 pt-4">
<Button variant="outline" @click="handleCancel"> <Button variant="outline" @click="handleCancel">
@@ -46,31 +46,31 @@ const actionConfig = computed(() => {
const configs = { const configs = {
delete: { delete: {
title: 'Hapus Data', title: 'Hapus Data',
message: 'Apakah Anda yakin ingin menghapus data ini? Tindakan ini tidak dapat dibatalkan.', message: 'Akan dilakukan penghapusan data',
confirmText: 'Hapus', confirmText: 'Hapus',
variant: 'destructive' as const, variant: 'destructive' as const,
}, },
deactivate: { deactivate: {
title: 'Nonaktifkan Data', title: 'Nonaktifkan Data',
message: 'Apakah Anda yakin ingin menonaktifkan data ini?', message: 'Akan dilakukan peng-nonaktifkan data',
confirmText: 'Nonaktifkan', confirmText: 'Nonaktifkan',
variant: 'warning' as const, variant: 'warning' as const,
}, },
activate: { activate: {
title: 'Aktifkan Data', title: 'Aktifkan Data',
message: 'Apakah Anda yakin ingin mengaktifkan data ini?', message: 'Akan dilakukan pengaktifkan data',
confirmText: 'Aktifkan', confirmText: 'Aktifkan',
variant: 'default' as const, variant: 'default' as const,
}, },
archive: { archive: {
title: 'Arsipkan Data', title: 'Arsipkan Data',
message: 'Apakah Anda yakin ingin mengarsipkan data ini?', message: 'Akan dilakukan pengarsipan data',
confirmText: 'Arsipkan', confirmText: 'Arsipkan',
variant: 'warning' as const, variant: 'warning' as const,
}, },
restore: { restore: {
title: 'Pulihkan Data', title: 'Pulihkan Data',
message: 'Apakah Anda yakin ingin memulihkan data ini?', message: 'Akan dilakukan pemulihan data',
confirmText: 'Pulihkan', confirmText: 'Pulihkan',
variant: 'default' as const, variant: 'default' as const,
}, },
@@ -107,6 +107,8 @@ const finalCancelText = computed(() => {
function handleConfirm() { function handleConfirm() {
if (props.record) { if (props.record) {
emit('confirm', props.record, props.action) emit('confirm', props.record, props.action)
} else {
emit('confirm', { id: 0 }, 'confirmed')
} }
emit('update:open', false) emit('update:open', false)
} }
@@ -119,11 +121,13 @@ function handleCancel() {
<template> <template>
<Confirmation <Confirmation
v-model:open="isOpen" :title="finalTitle" :message="finalMessage" :confirm-text="finalConfirmText" v-model:open="isOpen" :variant="actionConfig.variant" size="md"
:cancel-text="finalCancelText" :variant="actionConfig.variant" size="md" @confirm="handleConfirm" :title="finalTitle" :message="finalMessage"
:confirm-text="finalConfirmText" :cancel-text="finalCancelText"
:no-true-slot="$slots.default ? false : true"
@confirm="handleConfirm"
@cancel="handleCancel" @cancel="handleCancel"
> >
<!-- Slot untuk informasi tambahan record -->
<div v-if="record && $slots.default" class="mt-4 p-3 bg-muted rounded-md"> <div v-if="record && $slots.default" class="mt-4 p-3 bg-muted rounded-md">
<slot :record="record" :action="action" /> <slot :record="record" :action="action" />
</div> </div>
@@ -0,0 +1,95 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from './types'
import { ActionEvents } from './types'
interface Props {
rec: ListItemDto
size?: 'default' | 'sm' | 'lg'
}
const props = withDefaults(defineProps<Props>(), {
size: 'lg',
})
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: 'Submit',
onClick: () => {
submit()
},
icon: 'i-lucide-check',
},
{
label: 'Hapus',
onClick: () => {
del()
},
icon: 'i-lucide-trash',
},
]
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function submit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmSubmit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
</script>
<template>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
:size="size"
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>
@@ -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>
+4 -6
View File
@@ -42,12 +42,6 @@ export interface RefSearchNav {
onClear: () => void onClear: () => void
} }
export interface RefExportNav {
onExportPdf?: () => void
onExportCsv?: () => void
onExportExcel?: () => void
}
// prepared header for relatively common usage // prepared header for relatively common usage
export interface HeaderPrep { export interface HeaderPrep {
title?: string title?: string
@@ -79,10 +73,14 @@ export interface LinkItem {
} }
export const ActionEvents = { export const ActionEvents = {
showConfirmSubmit: 'showConfirmSubmit',
showConfirmDelete: 'showConfirmDel', showConfirmDelete: 'showConfirmDel',
showEdit: 'showEdit', showEdit: 'showEdit',
showDetail: 'showDetail', showDetail: 'showDetail',
showProcess: 'showProcess', showProcess: 'showProcess',
showVerify: 'showVerify',
showValidate: 'showValidate',
showPrint: 'showPrint',
} }
export interface DataTableLoader { export interface DataTableLoader {
+1 -1
View File
@@ -64,7 +64,7 @@ const settingClass = computed(() => {
} }
cls += ' [&:not(.preview)_.height-default]:pt-2 [&:not(.preview)_.height-default]:2xl:!pt-1.5 [&:not(.preview)_.height-compact]:!pt-1 ' cls += ' [&:not(.preview)_.height-default]:pt-2 [&:not(.preview)_.height-default]:2xl:!pt-1.5 [&:not(.preview)_.height-compact]:!pt-1 '
cls += '[&_textarea]:md:text-xs [&_textarea]:2xl:!text-sm ' cls += '[&_textarea]:md:text-xs [&_textarea]:2xl:!text-sm '
cls += '[&_label]:md:text-xs [&_label]:md:text-xs [&_label]:2xl:!text-sm' cls += '[&_label]:md:text-xs [&_label]:2xl:!text-sm '
return cls return cls
}) })
</script> </script>
@@ -1,4 +1,5 @@
export { default as Block } from './block.vue' export { default as Block } from './block.vue'
export { default as Colon } from './colon.vue'
export { default as Cell } from './cell.vue' export { default as Cell } from './cell.vue'
export { default as Label } from './label.vue' export { default as Label } from './label.vue'
export { default as Field } from './field.vue' export { default as Field } from './field.vue'
+3 -3
View File
@@ -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>
+3 -3
View File
@@ -52,8 +52,8 @@ const isOpen = computed({
> >
<DialogHeader> <DialogHeader>
<DialogTitle :class="`text-sm 2xl:text-base font-semibold flex ${titleClass || ''}`"> <DialogTitle :class="`text-sm 2xl:text-base font-semibold flex ${titleClass || ''}`">
<div class="me-2 pt-0.5"> <div v-if="props.titleIcon" class="me-2 pt-0.5">
<Icon v-if="props.titleIcon" :name="props.titleIcon" :class="`!pt-2`" /> <Icon :name="props.titleIcon" :class="`!pt-2`" />
</div> </div>
<div> <div>
{{ props.title }} {{ props.title }}
@@ -61,7 +61,7 @@ const isOpen = computed({
</DialogTitle> </DialogTitle>
<DialogDescription v-if="props.description">{{ props.description }}</DialogDescription> <DialogDescription v-if="props.description">{{ props.description }}</DialogDescription>
</DialogHeader> </DialogHeader>
<DialogDescription :class="sizeClass"> <DialogDescription :class="`${sizeClass} md:text-xs 2xl:text-sm`">
<slot /> <slot />
</DialogDescription> </DialogDescription>
</DialogContent> </DialogContent>
@@ -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>
+11 -1
View File
@@ -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 }
} }
+18
View File
@@ -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,14 @@ 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 +60,7 @@ export function useRBAC() {
hasReadAccess, hasReadAccess,
hasUpdateAccess, hasUpdateAccess,
hasDeleteAccess, hasDeleteAccess,
getPagePermissions,
} }
} }
+24
View File
@@ -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,
})
+10 -1
View File
@@ -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
@@ -151,4 +152,12 @@ export function printFormData(formData: FormData) {
} }
} }
console.log("-------------------------"); console.log("-------------------------");
} }
export function unauthorizedToast() {
toast({
title: 'Unauthorized',
description: 'You are not authorized to perform this action.',
variant: 'destructive',
})
}
+49
View File
@@ -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: '',
}
}
+41
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
export interface GenerateFile {
entityType_code: string
ref_id: number
type_code: string
}
+16
View File
@@ -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 }
+76
View File
@@ -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 }
+19
View File
@@ -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, }
+23
View File
@@ -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)
}
+15
View File
@@ -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)
}