feat(sep): refactor table letter

This commit is contained in:
riefive
2025-10-28 14:01:09 +07:00
parent 87762cf17a
commit aeaefb9478
7 changed files with 316 additions and 149 deletions
+3 -1
View File
@@ -332,7 +332,9 @@ onMounted(() => {
variant="outline"
type="button"
class="h-[40px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50"
@click="emit('event', 'search-letter')"
@click="
emit('event', 'search-letter', { admissionType, serviceType, search: referralLetterNumber || '' })
"
>
<Icon
name="i-lucide-search"
+53
View File
@@ -0,0 +1,53 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
const SelectedRadio = defineAsyncComponent(() => import('~/components/pub/my-ui/data/select-radio.vue'))
export interface LetterData {
letterNumber: string
plannedDate: string
sepNumber: string
patientName: string
bpjsCardNo: string
clinic: string
doctor: string
}
export const config: Config = {
cols: [{ width: 50 }, { width: 150 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 120 }, { width: 150 }, { width: 200 }],
headers: [
[
{ label: '' },
{ label: 'CONTROL LETTER NO.' },
{ label: 'PLANNED DATE' },
{ label: 'SEP NO.' },
{ label: 'PATIENT NAME' },
{ label: 'BPJS CARD NO.' },
{ label: 'CLINIC' },
{ label: 'DOCTOR' },
],
],
keys: ['check', 'letterNumber', 'plannedDate', 'sepNumber', 'patientName', 'bpjsCardNo', 'clinic', 'doctor'],
delKeyNames: [
{ key: 'code', label: 'Code' },
{ key: 'name', label: 'Name' },
],
parses: {},
components: {
check(rec, idx) {
return {
idx,
rec: { ...rec as object, menu: 'letter' },
component: SelectedRadio,
}
},
},
htmls: {},
}
+39
View File
@@ -0,0 +1,39 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { LetterData } from './list-cfg.letter'
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list-cfg.letter'
const props = defineProps<{
data: LetterData[]
selected?: string
paginationMeta?: PaginationMeta
}>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<PubMyUiDataTable
v-bind="config"
:rows="props.data"
:selected="props.selected"
/>
<PaginationView
v-if="paginationMeta"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</template>
@@ -1,104 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from '~/components/pub/ui/dialog'
import { Button } from '~/components/pub/ui/button'
import { Input } from '~/components/pub/ui/input'
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
const props = defineProps<{
open: boolean
letters: Array<{
noSurat: string
tglRencana: string
noSep: string
namaPasien: string
noBpjs: string
klinik: string
dokter: string
}>
selected: string
}>()
const emit = defineEmits<{
(e: 'update:open', value: boolean): void
(e: 'update:selected', value: string): void
(e: 'save'): void
}>()
const search = ref('')
const filteredLetters = computed(() => {
const letters = props.letters || []
return letters.filter((p) => p.noSurat.includes(search.value) || p.noSep.includes(search.value))
})
function saveSelection() {
emit('save')
emit('update:open', false)
}
</script>
<template>
<Dialog :open="props.open" @update:open="emit('update:open', $event)">
<DialogTrigger as-child></DialogTrigger>
<DialogContent class="max-w-[50%]">
<DialogHeader>
<DialogTitle>Cari No. Surat Kontrol</DialogTitle>
</DialogHeader>
<!-- Input Search -->
<div class="mb-2 max-w-[50%]">
<Input v-model="search" placeholder="Cari berdasarkan No. Surat Kontrol / No. SEP" />
</div>
<!-- Table -->
<div class="overflow-x-auto rounded-lg border">
<table class="w-full text-sm">
<thead class="bg-gray-100">
<tr class="text-left">
<th class="p-2"></th>
<th class="p-2">NO. SURAT KONTROL</th>
<th class="p-2">TGL RENCANA KONTROL</th>
<th class="p-2">NO. SEP</th>
<th class="p-2">NAMA PASIEN</th>
<th class="p-2">NO. KARTU BPJS</th>
<th class="p-2">KLINIK</th>
<th class="p-2">DOKTER</th>
</tr>
</thead>
<tbody class="font-normal">
<tr v-for="p in filteredLetters" :key="p.noSurat" class="border-t hover:bg-gray-50">
<td class="p-2">
<RadioGroup :model-value="props.selected" @update:model-value="emit('update:selected', $event)">
<RadioGroupItem :id="p.noSurat" :value="p.noSurat" />
</RadioGroup>
</td>
<td class="p-2">{{ p.noSurat }}</td>
<td class="p-2">{{ p.tglRencana }}</td>
<td class="p-2">{{ p.noSep }}</td>
<td class="p-2">{{ p.namaPasien }}</td>
<td class="p-2">{{ p.noBpjs }}</td>
<td class="p-2">{{ p.klinik }}</td>
<td class="p-2">{{ p.dokter }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Footer -->
<DialogFooter>
<Button variant="default" class="h-[40px] min-w-[120px] text-white" @click="saveSelection">
<Icon name="i-lucide-save" class="h-5 w-5" />
Simpan
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
+126
View File
@@ -0,0 +1,126 @@
<script setup lang="ts">
import { ref, provide, watch } from 'vue'
// Components
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from '~/components/pub/ui/dialog'
import { Button } from '~/components/pub/ui/button'
import { Input } from '~/components/pub/ui/input'
import ListLetter from './list-letter.vue'
// Types
import type { LetterData } from './list-cfg.letter'
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Helpers
import { refDebounced } from '@vueuse/core'
const props = defineProps<{
open: boolean
letters: Array<LetterData>
selected: string
paginationMeta?: PaginationMeta
}>()
const emit = defineEmits<{
(e: 'update:open', value: boolean): void
(e: 'update:selected', value: string): void
(e: 'fetch', value: any): void
(e: 'save'): void
}>()
const search = ref('')
const debouncedSearch = refDebounced(search, 500) // 500ms debounce
// Provide for radio selection - use selected prop directly
const recSelectId = ref<string>(props.selected || '')
const recSelectMenu = ref<string>('letter')
provide('rec_select_id', recSelectId)
provide('rec_select_menu', recSelectMenu)
function saveSelection() {
// Validate that a letter is selected
if (!props.selected || props.selected === '') {
console.warn('No letter selected')
return
}
emit('save')
emit('update:open', false)
}
function handlePageChange(page: number) {
emit('fetch', { 'page-number': page })
}
// Watch for changes in recSelectId and emit update:selected
watch(recSelectId, (newValue) => {
if (newValue && newValue !== '') {
emit('update:selected', newValue)
}
})
// Watch for changes in selected prop
watch(() => props.selected, (newValue) => {
recSelectId.value = newValue || ''
})
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>
<template>
<Dialog
:open="props.open"
@update:open="emit('update:open', $event)"
>
<DialogTrigger as-child></DialogTrigger>
<DialogContent class="max-w-3xl">
<DialogHeader>
<DialogTitle>Search Control Letter</DialogTitle>
</DialogHeader>
<!-- Input Search -->
<div class="max-w-[50%]">
<Input
v-model="search"
placeholder="Search by Control Letter No. / SEP No."
/>
</div>
<div class="overflow-x-auto rounded-lg border">
<ListLetter
:data="letters"
:selected="props.selected"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
<!-- Footer -->
<DialogFooter>
<Button
variant="default"
class="h-[40px] min-w-[120px] text-white"
@click="saveSelection"
>
<Icon
name="i-lucide-save"
class="h-5 w-5"
/>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
+90 -39
View File
@@ -6,6 +6,7 @@ import { useRoute } from 'vue-router'
import AppSepEntryForm from '~/components/app/sep/entry-form.vue'
import AppViewPatient from '~/components/app/patient/view-patient.vue'
import AppViewHistory from '~/components/app/sep/view-history.vue'
import AppViewLetter from '~/components/app/sep/view-letter.vue'
import { toast } from '~/components/pub/ui/toast'
// Types
@@ -33,17 +34,19 @@ import { getValueLabelList as getDistrictList } from '~/services/vclaim-region-d
import { getValueLabelList as getDoctorLabelList } from '~/services/vclaim-doctor.service'
import { getValueLabelList as getHealthFacilityLabelList } from '~/services/vclaim-healthcare.service'
import { getValueLabelList as getDiagnoseLabelList } from '~/services/vclaim-diagnose.service'
import { getList as getHospitalLetterList } from '~/services/vclaim-reference-hospital-letter.service'
import { getList as getControlLetterList } from '~/services/vclaim-control-letter.service'
import { getList as geMonitoringVisitList } from '~/services/vclaim-monitoring-visit.service'
import { getList as getMonitoringHistoryList } from '~/services/vclaim-monitoring-history.service'
import { create as createSep, makeSepData } from '~/services/vclaim-sep.service'
// Handlers
import {
patients,
selectedPatient,
selectedPatientObject,
paginationMeta,
getPatientsList,
import {
patients,
selectedPatient,
selectedPatientObject,
paginationMeta,
getPatientsList,
getPatientCurrent,
getPatientByIdentifierSearch,
} from '~/handlers/patient.handler'
@@ -57,6 +60,7 @@ const selectedObjects = ref<any>({})
const selectedServiceType = ref<string>('')
const histories = ref<Array<SepHistoryData>>([])
const visits = ref<Array<SepVisitData>>([])
const letters = ref<Array<any>>([])
const doctors = ref<Array<{ value: string | number; label: string }>>([])
const diagnoses = ref<Array<{ value: string | number; label: string }>>([])
const facilitiesFrom = ref<Array<{ value: string | number; label: string }>>([])
@@ -144,28 +148,68 @@ async function getMonitoringVisitMappers() {
}
}
const letters = [
{
noSurat: 'SK22334442',
tglRencana: '12 Agustus 2025',
noSep: 'SEP3232332',
namaPasien: 'Ahmad Baidowi',
noBpjs: '33442331214',
klinik: 'Penyakit Dalam',
dokter: 'dr. Andi Prasetyo, Sp.PD-KHOM',
},
{
noSurat: 'SK99120039',
tglRencana: '12 Agustus 2025',
noSep: 'SEP4443232',
namaPasien: 'Bian Maulana',
noBpjs: '33442367656',
klinik: 'Gigi',
dokter: 'dr. Achmad Suparjo',
},
]
async function getLetterMappers(admissionType: string, search: string) {
letters.value = []
let result = null
if (admissionType !== '3') {
result = await getHospitalLetterList({
letterNumber: search,
})
} else {
result = await getControlLetterList({
letterNumber: search,
mode: 'by-control',
})
if (result && result.success && result.body) {
const lettersRaw = result.body?.response || null
if (!lettersRaw) {
result = await getControlLetterList({
letterNumber: search,
mode: 'by-card',
})
}
}
if (result && result.success && result.body) {
const lettersRaw = result.body?.response || null
if (!lettersRaw) {
result = await getControlLetterList({
letterNumber: search,
mode: 'by-sep',
})
}
}
}
if (result && result.success && result.body) {
const lettersRaw = result.body?.response || null
console.log(lettersRaw)
// {
// noSurat: 'SK22334442',
// tglRencana: '12 Agustus 2025',
// noSep: 'SEP3232332',
// namaPasien: 'Ahmad Baidowi',
// noBpjs: '33442331214',
// klinik: 'Penyakit Dalam',
// dokter: 'dr. Andi Prasetyo, Sp.PD-KHOM',
// },
function handleSavePatient() {
// {
// noSurat: 'SK99120039',
// tglRencana: '12 Agustus 2025',
// noSep: 'SEP4443232',
// namaPasien: 'Bian Maulana',
// noBpjs: '33442367656',
// klinik: 'Gigi',
// dokter: 'dr. Achmad Suparjo',
// },
if (!lettersRaw) return
lettersRaw.forEach((result: any) => {
// letters.value.push({})
})
}
}
function handleSavePatient() {
selectedPatientObject.value = null
setTimeout(() => {
getPatientCurrent(selectedPatient.value)
@@ -196,10 +240,7 @@ async function handleEvent(menu: string, value: any) {
return
}
if (menu === 'search-letter') {
getMonitoringVisitMappers().then(() => {
openLetter.value = true
})
return
openLetter.value = true
}
if (menu === 'history-sep') {
getMonitoringHistoryMappers().then(() => {
@@ -401,15 +442,17 @@ onMounted(async () => {
v-model:selected="selectedPatient"
:patients="patients"
:pagination-meta="paginationMeta"
@fetch="(value) => {
if (value.search && value.search.length >= 3) {
// Use identifier search for specific searches (NIK, RM, etc.)
getPatientByIdentifierSearch(value.search)
} else {
// Use regular search for general searches
getPatientsList({ ...value, 'page-size': 10, includes: 'person' })
@fetch="
(value) => {
if (value.search && value.search.length >= 3) {
// Use identifier search for specific searches (NIK, RM, etc.)
getPatientByIdentifierSearch(value.search)
} else {
// Use regular search for general searches
getPatientsList({ ...value, 'page-size': 10, includes: 'person' })
}
}
}"
"
@save="handleSavePatient"
/>
<AppSepTableSearchLetter
@@ -422,4 +465,12 @@ onMounted(async () => {
v-model:open="openHistory"
:histories="histories"
/>
<AppViewLetter
v-model:open="openLetter"
:letters="letters"
:selected="selectedLetter"
:pagination-meta="paginationMeta"
@fetch="(value) => getLetterMappers(value.admissionType, value.search)"
@save="handleSaveLetter"
/>
</template>
@@ -6,16 +6,16 @@ const name = 'rencana-kontrol'
export function getList(params: any = null) {
let url = path
if (params?.letterNumber && params.letterMode === 'by-control') {
if (params?.letterNumber && params.mode === 'by-control') {
url += `/noSuratKontrol/${params.letterNumber}`
}
if (params?.letterNumber && params.letterMode === 'by-card') {
if (params?.letterNumber && params.mode === 'by-card') {
url += `/noka/${params.letterNumber}`
}
if (params?.letterNumber && params.letterMode === 'by-sep') {
if (params?.letterNumber && params.mode === 'by-sep') {
url += `/${params.letterNumber}`
}
if (params?.letterNumber && params.letterMode === 'by-schedule') {
if (params?.letterNumber && params.mode === 'by-schedule') {
url += `/jadwalDokter?jeniskontrol=${params.controlType}&kodepoli=${params.poliCode}&tanggalkontrol=${params.controlDate}`
delete params.controlType
delete params.poliCode
@@ -23,7 +23,7 @@ export function getList(params: any = null) {
}
if (params) {
delete params.letterNumber
delete params.letterMode
delete params.mode
}
return base.getList(url, params, name)
}