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">
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
}
// Components
import * as DE from '~/components/pub/my-ui/doc-entry'
// Props
const props = defineProps<{
installation: {
msg: {
placeholder: string
}
items: {
value: string
label: string
code: string
}[]
}
schema: any
initialValues?: Partial<InstallationFormData>
errors?: FormErrors
payments: any[]
units: any[]
visits: any[]
}>()
const emit = defineEmits<{
submit: [values: InstallationFormData, resetForm: () => void]
cancel: [resetForm: () => void]
(e: 'search', values: any): 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 onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: InstallationFormData = {
name: values.name || '',
code: values.code || '',
encounterClassCode: values.encounterClassCode || '',
}
emit('submit', formData, resetForm)
function handleReset() {
paymentItem.value = null
unitItem.value = null
visitItem.value = null
}
// Form cancel handler
function onCancelForm({ resetForm }: { resetForm: () => void }) {
emit('cancel', resetForm)
function handleSearch() {
emit('search', {
paymentMethodCode: paymentItem.value,
unit: unitItem.value,
visit: visitItem.value,
})
}
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>
<div class="mx-auto w-full">
<DE.Block
labelSize="thin"
class="!pt-0"
:colCount="1"
:cellFlex="false"
>
<DE.Cell>
<DE.Label height="compact">Cara Bayar</DE.Label>
<DE.Field>
<Select
id="paymentMethodCode"
v-model="paymentItem"
:items="payments || []"
placeholder="Pilih Jenis Pembayaran"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">Poliklinik</DE.Label>
<DE.Field>
<Select
id="unit"
v-model="unitItem"
:items="units || []"
placeholder="Pilih Poliklinik"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label height="compact">Kunjungan</DE.Label>
<DE.Field>
<Select
id="visit"
v-model="visitItem"
:items="visits || []"
placeholder="Pilih Kunjungan"
/>
</DE.Field>
</DE.Cell>
</DE.Block>
<div class="flex w-full justify-end gap-2">
<Button
variant="outline"
type="button"
class="h-[40px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50"
@click="handleReset"
>
<Icon
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>
+29 -11
View File
@@ -20,11 +20,13 @@ const props = defineProps<{
const emit = defineEmits<{
apply: [filters: { personName: string; startDate: string; endDate: string }]
click: []
}>()
const searchQuery = ref('')
const isRoleRegistration = props.activePositon === 'registration'
const isRoleMedical = props.activePositon === 'medical'
const debouncedSearchQuery = refDebounced(searchQuery, 500)
const df = new DateFormatter('en-US', {
dateStyle: 'medium',
@@ -43,14 +45,18 @@ const oneMonthAgoCalendar = new CalendarDate(
oneMonthAgo.getDate(),
)
const value = ref({
const dateRange = ref({
start: oneMonthAgoCalendar,
end: todayCalendar,
}) as Ref<DateRange>
function onFilterClick() {
const startDate = value.value.start ? value.value.start.toString() : ''
const endDate = value.value.end ? value.value.end.toString() : startDate
emit('click')
}
function onFilterApply() {
const startDate = dateRange.value.start ? dateRange.value.start.toString() : ''
const endDate = dateRange.value.end ? dateRange.value.end.toString() : startDate
emit('apply', {
personName: searchQuery.value,
@@ -58,6 +64,18 @@ function onFilterClick() {
endDate,
})
}
watch(debouncedSearchQuery, () => {
onFilterApply()
})
watch(
dateRange,
() => {
onFilterApply()
},
{ deep: true },
)
</script>
<template>
@@ -76,18 +94,18 @@ function onFilterClick() {
<Button
variant="outline"
: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" />
<template v-if="value.start">
<template v-if="value.end">
{{ df.format(value.start.toDate(getLocalTimeZone())) }} -
{{ df.format(value.end.toDate(getLocalTimeZone())) }}
<template v-if="dateRange.start">
<template v-if="dateRange.end">
{{ df.format(dateRange.start.toDate(getLocalTimeZone())) }} -
{{ df.format(dateRange.end.toDate(getLocalTimeZone())) }}
</template>
<template v-else>
{{ df.format(value.start.toDate(getLocalTimeZone())) }}
{{ df.format(dateRange.start.toDate(getLocalTimeZone())) }}
</template>
</template>
<template v-else>Pick a date</template>
@@ -95,10 +113,10 @@ function onFilterClick() {
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<RangeCalendar
v-model="value"
v-model="dateRange"
initial-focus
:number-of-months="2"
@update:start-value="(startDate) => (value.start = startDate)"
@update:start-value="(startDate) => (dateRange.start = startDate)"
/>
</PopoverContent>
</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 { useSidebar } from '~/components/pub/ui/sidebar/utils'
// Constants
import { paymentTypes } from '~/lib/constants.vclaim'
// App libs
import { getServicePosition } from '~/lib/roles' // previously getPositionAs
@@ -19,6 +22,7 @@ import {
remove as removeEncounter,
cancel as cancelEncounter,
} from '~/services/encounter.service'
import { getValueLabelList as getUnits } from '~/services/unit.service'
// Handlers
import { uploadAttachmentCustom } from '~/handlers/supporting-document.handler'
@@ -64,6 +68,12 @@ const isFilterFormDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
const isRecordCancelOpen = ref(false)
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
const hreaderPrep: CH.Config = {
@@ -80,31 +90,6 @@ if (!props.canCreate) {
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
provide('activeServicePosition', activeServicePosition)
provide('rec_id', recId)
@@ -153,8 +138,13 @@ watch([recSepId, recSepMenu, recSepSubMenu], (value) => {
}
})
onMounted(() => {
onMounted(async () => {
getPatientList()
units.value = await getUnits({}, true)
payments.value = Object.keys(paymentTypes).map((item) => ({
value: item.toString(),
label: paymentTypes[item],
})) as any
})
/////// Functions
@@ -350,6 +340,7 @@ function handleRemoveConfirmation() {
<FilterNav
:active-positon="activeServicePosition"
@apply="handleFilterApply"
@click="() => (isFilterFormDialogOpen = true)"
@onExportPdf="() => {}"
@onExportExcel="() => {}"
@nExportCsv="() => {}"
@@ -364,11 +355,15 @@ function handleRemoveConfirmation() {
<!-- Filter -->
<Dialog
v-model:open="isFilterFormDialogOpen"
title="Filter"
title="Filter Data"
size="lg"
prevent-outside
>
<FilterForm v-bind="filter" />
<FilterForm
:payments="payments"
:units="units"
:visits="visits"
/>
</Dialog>
<!-- Batal -->