feat: Implement encounter list with filtering and navigation components.

This commit is contained in:
riefive
2025-12-08 14:51:38 +07:00
parent 703d3f902a
commit 4fc48d6131
4 changed files with 257 additions and 137 deletions
@@ -0,0 +1,114 @@
<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'
interface InstallationFormData {
name: string
code: string
encounterClassCode: string
}
const props = defineProps<{
installation: {
msg: {
placeholder: string
}
items: {
value: string
label: string
code: string
}[]
}
schema: any
initialValues?: Partial<InstallationFormData>
errors?: FormErrors
}>()
const emit = defineEmits<{
submit: [values: InstallationFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: InstallationFormData = {
name: values.name || '',
code: values.code || '',
encounterClassCode: values.encounterClassCode || '',
}
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"
:initial-values="initialValues"
>
<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">
<FieldGroup>
<Label label-for="parentId">Cara Bayar</Label>
<Field id="encounterClassCode" :errors="errors">
<FormField v-slot="{ componentField }" name="encounterClassCode">
<FormItem>
<FormControl>
<Select v-bind="componentField" :items="items" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="parentId">Poliklinik</Label>
<Field id="encounterClassCode" :errors="errors">
<FormField v-slot="{ componentField }" name="encounterClassCode">
<FormItem>
<FormControl>
<Select v-bind="componentField" :items="items" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<FieldGroup>
<Label label-for="parentId">Kunjungan</Label>
<Field id="encounterClassCode" :errors="errors">
<FormField v-slot="{ componentField }" name="encounterClassCode">
<FormItem>
<FormControl>
<Select v-bind="componentField" :items="items" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
</div>
</div>
</form>
</Form>
</template>
+91 -98
View File
@@ -1,114 +1,107 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormErrors } from '~/types/error' // Components
import { toTypedSchema } from '@vee-validate/zod' import * as DE from '~/components/pub/my-ui/doc-entry'
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'
interface InstallationFormData {
name: string
code: string
encounterClassCode: string
}
// Props
const props = defineProps<{ const props = defineProps<{
installation: { payments: any[]
msg: { units: any[]
placeholder: string visits: any[]
}
items: {
value: string
label: string
code: string
}[]
}
schema: any
initialValues?: Partial<InstallationFormData>
errors?: FormErrors
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
submit: [values: InstallationFormData, resetForm: () => void] (e: 'search', values: any): void
cancel: [resetForm: () => void]
}>() }>()
const formSchema = toTypedSchema(props.schema) const paymentItem = ref<any>(null)
const unitItem = ref<any>(null)
const visitItem = ref<any>(null)
// Form submission handler function handleReset() {
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) { paymentItem.value = null
const formData: InstallationFormData = { unitItem.value = null
name: values.name || '', visitItem.value = null
code: values.code || '',
encounterClassCode: values.encounterClassCode || '',
}
emit('submit', formData, resetForm)
} }
// Form cancel handler function handleSearch() {
function onCancelForm({ resetForm }: { resetForm: () => void }) { emit('search', {
emit('cancel', resetForm) paymentMethodCode: paymentItem.value,
unit: unitItem.value,
visit: visitItem.value,
})
} }
const items = ref([
{ label: 'Rujukan Internal', value: 'ri' },
{ label: 'SEP Rujukan', value: 'sr' },
])
</script> </script>
<template> <template>
<Form <div class="mx-auto w-full">
v-slot="{ handleSubmit, resetForm }" <DE.Block
as="" labelSize="thin"
keep-values class="!pt-0"
:validation-schema="formSchema" :colCount="1"
:initial-values="initialValues" :cellFlex="false"
> >
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))"> <DE.Cell>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl"> <DE.Label height="compact">Cara Bayar</DE.Label>
<div class="flex flex-col justify-between"> <DE.Field>
<FieldGroup> <Select
<Label label-for="parentId">Cara Bayar</Label> id="paymentMethodCode"
<Field id="encounterClassCode" :errors="errors"> v-model="paymentItem"
<FormField v-slot="{ componentField }" name="encounterClassCode"> :items="payments || []"
<FormItem> placeholder="Pilih Jenis Pembayaran"
<FormControl> />
<Select v-bind="componentField" :items="items" /> </DE.Field>
</FormControl> </DE.Cell>
<FormMessage />
</FormItem> <DE.Cell>
</FormField> <DE.Label height="compact">Poliklinik</DE.Label>
</Field> <DE.Field>
</FieldGroup> <Select
<FieldGroup> id="unit"
<Label label-for="parentId">Poliklinik</Label> v-model="unitItem"
<Field id="encounterClassCode" :errors="errors"> :items="units || []"
<FormField v-slot="{ componentField }" name="encounterClassCode"> placeholder="Pilih Poliklinik"
<FormItem> />
<FormControl> </DE.Field>
<Select v-bind="componentField" :items="items" /> </DE.Cell>
</FormControl>
<FormMessage /> <DE.Cell>
</FormItem> <DE.Label height="compact">Kunjungan</DE.Label>
</FormField> <DE.Field>
</Field> <Select
</FieldGroup> id="visit"
<FieldGroup> v-model="visitItem"
<Label label-for="parentId">Kunjungan</Label> :items="visits || []"
<Field id="encounterClassCode" :errors="errors"> placeholder="Pilih Kunjungan"
<FormField v-slot="{ componentField }" name="encounterClassCode"> />
<FormItem> </DE.Field>
<FormControl> </DE.Cell>
<Select v-bind="componentField" :items="items" /> </DE.Block>
</FormControl>
<FormMessage /> <div class="flex w-full justify-end gap-2">
</FormItem> <Button
</FormField> variant="outline"
</Field> type="button"
</FieldGroup> class="h-[40px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50"
</div> @click="handleReset"
</div> >
</form> <Icon
</Form> name="i-lucide-rotate-ccw"
class="h-5 w-5"
/>
Reset
</Button>
<Button
variant="outline"
type="button"
class="h-[40px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50"
@click="handleSearch"
>
<Icon
name="i-lucide-check"
class="h-5 w-5"
/>
Terapkan
</Button>
</div>
</div>
</template> </template>
+29 -11
View File
@@ -20,11 +20,13 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
apply: [filters: { personName: string; startDate: string; endDate: string }] apply: [filters: { personName: string; startDate: string; endDate: string }]
click: []
}>() }>()
const searchQuery = ref('') const searchQuery = ref('')
const isRoleRegistration = props.activePositon === 'registration' const isRoleRegistration = props.activePositon === 'registration'
const isRoleMedical = props.activePositon === 'medical' const isRoleMedical = props.activePositon === 'medical'
const debouncedSearchQuery = refDebounced(searchQuery, 500)
const df = new DateFormatter('en-US', { const df = new DateFormatter('en-US', {
dateStyle: 'medium', dateStyle: 'medium',
@@ -43,14 +45,18 @@ const oneMonthAgoCalendar = new CalendarDate(
oneMonthAgo.getDate(), oneMonthAgo.getDate(),
) )
const value = ref({ const dateRange = ref({
start: oneMonthAgoCalendar, start: oneMonthAgoCalendar,
end: todayCalendar, end: todayCalendar,
}) as Ref<DateRange> }) as Ref<DateRange>
function onFilterClick() { function onFilterClick() {
const startDate = value.value.start ? value.value.start.toString() : '' emit('click')
const endDate = value.value.end ? value.value.end.toString() : startDate }
function onFilterApply() {
const startDate = dateRange.value.start ? dateRange.value.start.toString() : ''
const endDate = dateRange.value.end ? dateRange.value.end.toString() : startDate
emit('apply', { emit('apply', {
personName: searchQuery.value, personName: searchQuery.value,
@@ -58,6 +64,18 @@ function onFilterClick() {
endDate, endDate,
}) })
} }
watch(debouncedSearchQuery, () => {
onFilterApply()
})
watch(
dateRange,
() => {
onFilterApply()
},
{ deep: true },
)
</script> </script>
<template> <template>
@@ -76,18 +94,18 @@ function onFilterClick() {
<Button <Button
variant="outline" variant="outline"
:class=" :class="
cn('min-w-[240px] max-w-[320px] justify-start text-left font-normal', !value && 'text-muted-foreground') cn('min-w-[240px] max-w-[320px] justify-start text-left font-normal', !dateRange && 'text-muted-foreground')
" "
> >
<CalendarIcon class="mr-2 h-4 w-4" /> <CalendarIcon class="mr-2 h-4 w-4" />
<template v-if="value.start"> <template v-if="dateRange.start">
<template v-if="value.end"> <template v-if="dateRange.end">
{{ df.format(value.start.toDate(getLocalTimeZone())) }} - {{ df.format(dateRange.start.toDate(getLocalTimeZone())) }} -
{{ df.format(value.end.toDate(getLocalTimeZone())) }} {{ df.format(dateRange.end.toDate(getLocalTimeZone())) }}
</template> </template>
<template v-else> <template v-else>
{{ df.format(value.start.toDate(getLocalTimeZone())) }} {{ df.format(dateRange.start.toDate(getLocalTimeZone())) }}
</template> </template>
</template> </template>
<template v-else>Pick a date</template> <template v-else>Pick a date</template>
@@ -95,10 +113,10 @@ function onFilterClick() {
</PopoverTrigger> </PopoverTrigger>
<PopoverContent class="w-auto p-0"> <PopoverContent class="w-auto p-0">
<RangeCalendar <RangeCalendar
v-model="value" v-model="dateRange"
initial-focus initial-focus
:number-of-months="2" :number-of-months="2"
@update:start-value="(startDate) => (value.start = startDate)" @update:start-value="(startDate) => (dateRange.start = startDate)"
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
+23 -28
View File
@@ -10,6 +10,9 @@ import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confi
import FileUpload from '~/components/pub/my-ui/form/file-field.vue' import FileUpload from '~/components/pub/my-ui/form/file-field.vue'
import { useSidebar } from '~/components/pub/ui/sidebar/utils' import { useSidebar } from '~/components/pub/ui/sidebar/utils'
// Constants
import { paymentTypes } from '~/lib/constants.vclaim'
// App libs // App libs
import { getServicePosition } from '~/lib/roles' // previously getPositionAs import { getServicePosition } from '~/lib/roles' // previously getPositionAs
@@ -19,6 +22,7 @@ import {
remove as removeEncounter, remove as removeEncounter,
cancel as cancelEncounter, cancel as cancelEncounter,
} from '~/services/encounter.service' } from '~/services/encounter.service'
import { getValueLabelList as getUnits } from '~/services/unit.service'
// Handlers // Handlers
import { uploadAttachmentCustom } from '~/handlers/supporting-document.handler' import { uploadAttachmentCustom } from '~/handlers/supporting-document.handler'
@@ -64,6 +68,12 @@ const isFilterFormDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false) const isRecordConfirmationOpen = ref(false)
const isRecordCancelOpen = ref(false) const isRecordCancelOpen = ref(false)
const uploadFile = ref<any>(null) const uploadFile = ref<any>(null)
const payments = ref<any>([])
const units = ref<any>([])
const visits = ref([
{ label: 'Rujukan Internal', value: 'ri' },
{ label: 'SEP Rujukan', value: 'sr' },
])
// Headers // Headers
const hreaderPrep: CH.Config = { const hreaderPrep: CH.Config = {
@@ -80,31 +90,6 @@ if (!props.canCreate) {
delete hreaderPrep.addNav delete hreaderPrep.addNav
} }
// Filters
const filter = ref<{
installation: {
msg: {
placeholder: string
}
items: {
value: string
label: string
code: string
}[]
}
schema: any
initialValues?: Partial<any>
errors?: any
}>({
installation: {
msg: {
placeholder: 'Pilih',
},
items: [],
},
schema: {},
})
// Recrod reactivities // Recrod reactivities
provide('activeServicePosition', activeServicePosition) provide('activeServicePosition', activeServicePosition)
provide('rec_id', recId) provide('rec_id', recId)
@@ -153,8 +138,13 @@ watch([recSepId, recSepMenu, recSepSubMenu], (value) => {
} }
}) })
onMounted(() => { onMounted(async () => {
getPatientList() getPatientList()
units.value = await getUnits({}, true)
payments.value = Object.keys(paymentTypes).map((item) => ({
value: item.toString(),
label: paymentTypes[item],
})) as any
}) })
/////// Functions /////// Functions
@@ -350,6 +340,7 @@ function handleRemoveConfirmation() {
<FilterNav <FilterNav
:active-positon="activeServicePosition" :active-positon="activeServicePosition"
@apply="handleFilterApply" @apply="handleFilterApply"
@click="() => (isFilterFormDialogOpen = true)"
@onExportPdf="() => {}" @onExportPdf="() => {}"
@onExportExcel="() => {}" @onExportExcel="() => {}"
@nExportCsv="() => {}" @nExportCsv="() => {}"
@@ -364,11 +355,15 @@ function handleRemoveConfirmation() {
<!-- Filter --> <!-- Filter -->
<Dialog <Dialog
v-model:open="isFilterFormDialogOpen" v-model:open="isFilterFormDialogOpen"
title="Filter" title="Filter Data"
size="lg" size="lg"
prevent-outside prevent-outside
> >
<FilterForm v-bind="filter" /> <FilterForm
:payments="payments"
:units="units"
:visits="visits"
/>
</Dialog> </Dialog>
<!-- Batal --> <!-- Batal -->