fix(sep): integrate search + pagination on patient
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import { refDebounced } from '@vueuse/core'
|
||||||
|
|
||||||
|
// Components
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -11,44 +16,60 @@ import {
|
|||||||
import { Button } from '~/components/pub/ui/button'
|
import { Button } from '~/components/pub/ui/button'
|
||||||
import { Input } from '~/components/pub/ui/input'
|
import { Input } from '~/components/pub/ui/input'
|
||||||
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
||||||
|
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
open: boolean
|
open: boolean
|
||||||
patients: Array<{
|
patients: Array<{
|
||||||
ktp: string
|
identity: string
|
||||||
rm: string
|
number: string
|
||||||
bpjs: string
|
bpjs: string
|
||||||
nama: string
|
name: string
|
||||||
}>
|
}>
|
||||||
selected: string
|
selected: string
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:open', value: boolean): void
|
(e: 'update:open', value: boolean): void
|
||||||
(e: 'update:selected', value: string): void
|
(e: 'update:selected', value: string): void
|
||||||
|
(e: 'fetch', value: any): void
|
||||||
(e: 'save'): void
|
(e: 'save'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
|
const debouncedSearch = refDebounced(search, 500) // 500ms debounce
|
||||||
|
|
||||||
const filteredPatients = computed(() => {
|
const filteredPatients = computed(() => {
|
||||||
const patients = props.patients || []
|
const patients = props.patients || []
|
||||||
return patients.filter(
|
return patients
|
||||||
(p) =>
|
|
||||||
p.ktp.includes(search.value) ||
|
|
||||||
p.rm.includes(search.value) ||
|
|
||||||
p.bpjs.includes(search.value) ||
|
|
||||||
p.nama.toLowerCase().includes(search.value.toLowerCase()),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function saveSelection() {
|
function saveSelection() {
|
||||||
emit('save')
|
emit('save')
|
||||||
emit('update:open', false)
|
emit('update:open', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('fetch', { page })
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(debouncedSearch, (newValue) => {
|
||||||
|
// Only search if 3+ characters or empty (to clear search)
|
||||||
|
if (newValue.length === 0 || newValue.length >= 3) {
|
||||||
|
emit('fetch', { search: newValue })
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog :open="props.open" @update:open="emit('update:open', $event)">
|
<Dialog
|
||||||
|
:open="props.open"
|
||||||
|
@update:open="emit('update:open', $event)"
|
||||||
|
>
|
||||||
<DialogTrigger as-child></DialogTrigger>
|
<DialogTrigger as-child></DialogTrigger>
|
||||||
<DialogContent class="max-w-3xl">
|
<DialogContent class="max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -57,7 +78,10 @@ function saveSelection() {
|
|||||||
|
|
||||||
<!-- Input Search -->
|
<!-- Input Search -->
|
||||||
<div class="mb-2 max-w-[50%]">
|
<div class="mb-2 max-w-[50%]">
|
||||||
<Input v-model="search" placeholder="Cari berdasarkan No. KTP / No. RM / Nomor Kartu" />
|
<Input
|
||||||
|
v-model="search"
|
||||||
|
placeholder="Cari berdasarkan No. KTP / No. RM / Nomor Kartu"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table -->
|
<!-- Table -->
|
||||||
@@ -73,25 +97,46 @@ function saveSelection() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="font-normal">
|
<tbody class="font-normal">
|
||||||
<tr v-for="p in filteredPatients" :key="p.ktp" class="border-t hover:bg-gray-50">
|
<tr
|
||||||
|
v-for="p in filteredPatients"
|
||||||
|
:key="p.identity"
|
||||||
|
class="border-t hover:bg-gray-50"
|
||||||
|
>
|
||||||
<td class="p-2">
|
<td class="p-2">
|
||||||
<RadioGroup :model-value="props.selected" @update:model-value="emit('update:selected', $event)">
|
<RadioGroup
|
||||||
<RadioGroupItem :id="p.ktp" :value="p.ktp" />
|
:model-value="props.selected"
|
||||||
|
@update:model-value="emit('update:selected', $event)"
|
||||||
|
>
|
||||||
|
<RadioGroupItem
|
||||||
|
:id="p.identity"
|
||||||
|
:value="p.identity"
|
||||||
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</td>
|
</td>
|
||||||
<td class="p-2">{{ p.ktp }}</td>
|
<td class="p-2">{{ p.identity }}</td>
|
||||||
<td class="p-2">{{ p.rm }}</td>
|
<td class="p-2">{{ p.number }}</td>
|
||||||
<td class="p-2">{{ p.bpjs }}</td>
|
<td class="p-2">{{ p.bpjs }}</td>
|
||||||
<td class="p-2">{{ p.nama }}</td>
|
<td class="p-2">{{ p.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<PaginationView
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="default" class="h-[40px] min-w-[120px] text-white" @click="saveSelection">
|
<Button
|
||||||
<Icon name="i-lucide-save" class="h-5 w-5" />
|
variant="default"
|
||||||
|
class="h-[40px] min-w-[120px] text-white"
|
||||||
|
@click="saveSelection"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="i-lucide-save"
|
||||||
|
class="h-5 w-5"
|
||||||
|
/>
|
||||||
Simpan
|
Simpan
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -1,34 +1,68 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||||
|
|
||||||
|
// Services
|
||||||
import { getPatients } from '~/services/patient.service'
|
import { getPatients } from '~/services/patient.service'
|
||||||
|
|
||||||
const openPatient = ref(false)
|
const openPatient = ref(false)
|
||||||
const openLetter = ref(false)
|
const openLetter = ref(false)
|
||||||
const openHistory = ref(false)
|
const openHistory = ref(false)
|
||||||
const selectedPatient = ref('3456512345678880')
|
const selectedPatient = ref('')
|
||||||
const selectedLetter = ref('SK22334442')
|
const selectedLetter = ref('SK22334442')
|
||||||
|
|
||||||
// patients used by AppSepTableSearchPatient (will be filled from API)
|
// patients used by AppSepTableSearchPatient (will be filled from API)
|
||||||
const patients = ref<Array<{ ktp: string; rm: string; bpjs: string; nama: string }>>([
|
const patients = ref<Array<{ identity: string; number: string; bpjs: string; name: string }>>([
|
||||||
// fallback empty list until fetched
|
// fallback empty list until fetched
|
||||||
])
|
])
|
||||||
const isPatientsLoading = ref(false)
|
const isPatientsLoading = ref(false)
|
||||||
|
const paginationMeta = ref<PaginationMeta>({
|
||||||
|
recordCount: 0,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
totalPage: 5,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrev: false,
|
||||||
|
})
|
||||||
|
|
||||||
function mapPatientToRow(p: any) {
|
function mapPatientToRow(patient: any) {
|
||||||
// Defensive mapping: try common field names that might be returned by the API
|
// Defensive mapping: try common field names that might be returned by the API
|
||||||
const ktp = p.ktp || p.noKtp || p.ktpNumber || p.person?.identityNo || ''
|
const identity = patient?.person?.residentIdentityNumber || '-'
|
||||||
const rm = p.rm || p.noRm || p.medicalRecordNo || ''
|
const number = patient?.number || patient?.medicalRecordNo || '-'
|
||||||
const bpjs = p.bpjs || p.noBpjs || p.bpjsNo || ''
|
const bpjs = patient?.person?.refNumber || '-'
|
||||||
const nama = p.nama || p.name || p.fullName || p.person?.name || p.person?.fullName || p.person?.nama || ''
|
const name = patient.name || patient?.person?.name || '-'
|
||||||
return { ktp, rm, bpjs, nama }
|
return { identity, number, bpjs, name }
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapPaginationMetaToRow(meta: any) {
|
||||||
|
const recordCount = meta['record_totalCount'] ? Number(meta['record_totalCount']) : 0
|
||||||
|
const currentCount = meta['record_currentCount'] ? Number(meta['record_currentCount']) : 0
|
||||||
|
const page = meta['page_number'] ? Number(meta['page_number']) : 1
|
||||||
|
const pageSize = meta['page_size'] ? Number(meta['page_size']) : 10
|
||||||
|
const totalPage = Math.ceil(recordCount / pageSize)
|
||||||
|
|
||||||
|
return {
|
||||||
|
recordCount,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPage,
|
||||||
|
hasNext: currentCount < recordCount && page < totalPage,
|
||||||
|
hasPrev: page > 1,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPatients(params: any = { 'page-size': 100 }) {
|
async function fetchPatients(params: any = { 'page-size': 100 }) {
|
||||||
try {
|
try {
|
||||||
isPatientsLoading.value = true
|
isPatientsLoading.value = true
|
||||||
|
patients.value = []
|
||||||
|
paginationMeta.value = {} as PaginationMeta
|
||||||
const result = await getPatients(params)
|
const result = await getPatients(params)
|
||||||
if (result && result.success && result.body && Array.isArray(result.body.data)) {
|
if (result && result.success && result.body && Array.isArray(result.body.data)) {
|
||||||
|
const meta = result.body.meta
|
||||||
patients.value = result.body.data.map(mapPatientToRow)
|
patients.value = result.body.data.map(mapPatientToRow)
|
||||||
|
paginationMeta.value = mapPaginationMetaToRow(meta)
|
||||||
} else {
|
} else {
|
||||||
// fallback to empty array
|
// fallback to empty array
|
||||||
patients.value = []
|
patients.value = []
|
||||||
@@ -64,20 +98,20 @@ const letters = [
|
|||||||
|
|
||||||
const histories = [
|
const histories = [
|
||||||
{
|
{
|
||||||
no_sep: "SP23311224",
|
no_sep: 'SP23311224',
|
||||||
tgl_sep: "12 Agustus 2025",
|
tgl_sep: '12 Agustus 2025',
|
||||||
no_rujukan: "123444",
|
no_rujukan: '123444',
|
||||||
diagnosis: "C34.9 – Karsinoma Paru",
|
diagnosis: 'C34.9 – Karsinoma Paru',
|
||||||
pelayanan: "Rawat Jalan",
|
pelayanan: 'Rawat Jalan',
|
||||||
kelas: "Kelas II",
|
kelas: 'Kelas II',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
no_sep: "SP23455667",
|
no_sep: 'SP23455667',
|
||||||
tgl_sep: "11 Agustus 2025",
|
tgl_sep: '11 Agustus 2025',
|
||||||
no_rujukan: "2331221",
|
no_rujukan: '2331221',
|
||||||
diagnosis: "K35 – Apendisitis akut",
|
diagnosis: 'K35 – Apendisitis akut',
|
||||||
pelayanan: "Rawat Jalan",
|
pelayanan: 'Rawat Jalan',
|
||||||
kelas: "Kelas II",
|
kelas: 'Kelas II',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -92,7 +126,7 @@ function handleSaveLetter() {
|
|||||||
function handleEvent(value: string) {
|
function handleEvent(value: string) {
|
||||||
if (value === 'search-patient') {
|
if (value === 'search-patient') {
|
||||||
// fetch patients from API then open the dialog
|
// fetch patients from API then open the dialog
|
||||||
fetchPatients().then(() => {
|
fetchPatients({ 'page-size': 10, includes: 'person' }).then(() => {
|
||||||
openPatient.value = true
|
openPatient.value = true
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -113,14 +147,20 @@ function handleEvent(value: string) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||||
<Icon name="i-lucide-panel-bottom" class="me-2" />
|
<Icon
|
||||||
<span class="font-semibold">Tambah</span> SEP
|
name="i-lucide-panel-bottom"
|
||||||
|
class="me-2"
|
||||||
|
/>
|
||||||
|
<span class="font-semibold">Tambah</span>
|
||||||
|
SEP
|
||||||
</div>
|
</div>
|
||||||
<AppSepEntryForm @event="handleEvent" />
|
<AppSepEntryForm @event="handleEvent" />
|
||||||
<AppSepTableSearchPatient
|
<AppSepTableSearchPatient
|
||||||
v-model:open="openPatient"
|
v-model:open="openPatient"
|
||||||
v-model:selected="selectedPatient"
|
v-model:selected="selectedPatient"
|
||||||
:patients="patients"
|
:patients="patients"
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@fetch="(value) => fetchPatients(value)"
|
||||||
@save="handleSavePatient"
|
@save="handleSavePatient"
|
||||||
/>
|
/>
|
||||||
<AppSepTableSearchLetter
|
<AppSepTableSearchLetter
|
||||||
@@ -133,5 +173,4 @@ function handleEvent(value: string) {
|
|||||||
v-model:open="openHistory"
|
v-model:open="openHistory"
|
||||||
:histories="histories"
|
:histories="histories"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user