feat(satusehat): add patient list components and integration
- Implement badge components for patient and status display - Create list component with configurable table columns - Add entry form for new patient registration - Integrate with existing SatuSehat service flow
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
rec: any
|
||||
idx?: number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-center">
|
||||
<p class="font-semibold text-sm">{{ props.rec.patient.name }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ props.rec.patient.mrn }}</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import Badge from './badge.vue'
|
||||
import { rowStatus } from './list-cfg'
|
||||
|
||||
const props = defineProps<{
|
||||
rec: { status: number }
|
||||
idx?: number
|
||||
}>()
|
||||
|
||||
const variants = {
|
||||
0: 'error',
|
||||
1: 'warning',
|
||||
2: 'info',
|
||||
} as const
|
||||
|
||||
const statusText = computed(() => {
|
||||
return rowStatus[props.rec.status as keyof typeof rowStatus]
|
||||
})
|
||||
|
||||
const badgeStatus = computed((): 'error' | 'warning' | 'success' | 'info' => {
|
||||
return variants[props.rec.status as keyof typeof variants] ?? 'info'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex">
|
||||
<Badge :status="badgeStatus" :text="statusText" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
interface BadgeProps {
|
||||
status: 'success' | 'info' | 'warning' | 'error'
|
||||
text: string
|
||||
}
|
||||
|
||||
const props = defineProps<BadgeProps>()
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
success: 'rounded-full border-transparent bg-green-500 text-white hover:bg-green-600',
|
||||
info: 'rounded-full border-transparent bg-blue-500 text-white hover:bg-blue-600',
|
||||
warning: 'rounded-full border-transparent bg-yellow-500 text-white hover:bg-yellow-600',
|
||||
error: 'rounded-full border-transparent bg-red-500 text-white hover:bg-red-600',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'info',
|
||||
},
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex">
|
||||
<div :class="cn(badgeVariants({ variant: props.status }))">
|
||||
{{ props.text }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import Block from '~/components/pub/form/block.vue'
|
||||
import FieldGroup from '~/components/pub/form/field-group.vue'
|
||||
import Field from '~/components/pub/form/field.vue'
|
||||
import Label from '~/components/pub/form/label.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form id="entry-form">
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||
<Icon name="i-lucide-user" class="me-2" />
|
||||
<span class="font-semibold">Tambah</span> Pasien
|
||||
</div>
|
||||
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||
<Block>
|
||||
<FieldGroup :column="3">
|
||||
<Label>Nama</Label>
|
||||
<Field>
|
||||
<Input type="text" name="name" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldGroup :column="3">
|
||||
<Label>Nama</Label>
|
||||
<Field>
|
||||
<Input type="text" name="name" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldGroup :column="3">
|
||||
<Label>Nomor RM</Label>
|
||||
<Field>
|
||||
<Input type="text" name="name" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldGroup>
|
||||
<Label dynamic>Alamat</Label>
|
||||
<Field>
|
||||
<Input type="text" name="name" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</Block>
|
||||
</div>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<PubNavFooterCsd />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { Col, KeyLabel, RecComponent, RecStrFuncComponent, RecStrFuncUnknown, Th } from '../../pub/nav/types'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
type SmallDetailDto = any
|
||||
|
||||
export const rowType = {
|
||||
1: 'Patient',
|
||||
2: 'Encounter',
|
||||
3: 'Observation',
|
||||
}
|
||||
|
||||
export const rowStatus = {
|
||||
0: 'Gagal',
|
||||
1: 'Pending',
|
||||
2: 'Terkirim',
|
||||
}
|
||||
|
||||
const action = defineAsyncComponent(() => import('~/components/pub/nav/dropdown-action-dud.vue'))
|
||||
const patientBadge = defineAsyncComponent(() => import('./badge-patient.vue'))
|
||||
const statusBadge = defineAsyncComponent(() => import('./badge-status.vue'))
|
||||
|
||||
export const cols: Col[] = [
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
]
|
||||
|
||||
export const header: Th[][] = [
|
||||
[
|
||||
{ label: 'ID' },
|
||||
{ label: 'Jenis' },
|
||||
{ label: 'Pasien' },
|
||||
{ label: 'Status' },
|
||||
{ label: 'Terakhir Update' },
|
||||
{ label: 'FHIR ID' },
|
||||
{ label: '' },
|
||||
],
|
||||
]
|
||||
|
||||
export const keys = ['id', 'resource_type', 'patient', 'status', 'updated_at', 'fhir_id', 'action']
|
||||
|
||||
export const delKeyNames: KeyLabel[] = [
|
||||
{ key: 'code', label: 'Kode' },
|
||||
{ key: 'name', label: 'Nama' },
|
||||
]
|
||||
|
||||
export const funcParsed: RecStrFuncUnknown = {
|
||||
name: (rec: unknown): unknown => {
|
||||
const recX = rec as SmallDetailDto
|
||||
return `${recX.firstName} ${recX.middleName || ''} ${recX.lastName || ''}`
|
||||
},
|
||||
}
|
||||
|
||||
export const funcComponent: RecStrFuncComponent = {
|
||||
patient(rec, idx) {
|
||||
const res: RecComponent = {
|
||||
idx,
|
||||
rec: rec as object,
|
||||
component: patientBadge,
|
||||
}
|
||||
return res
|
||||
},
|
||||
status(rec, idx) {
|
||||
const res: RecComponent = {
|
||||
idx,
|
||||
rec: rec as object,
|
||||
component: statusBadge,
|
||||
}
|
||||
return res
|
||||
},
|
||||
action(rec, idx) {
|
||||
const res: RecComponent = {
|
||||
idx,
|
||||
rec: rec as object,
|
||||
component: action,
|
||||
}
|
||||
return res
|
||||
},
|
||||
}
|
||||
|
||||
export const funcHtml: RecStrFuncUnknown = {
|
||||
patient_address(_rec) {
|
||||
return '-'
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
|
||||
|
||||
defineProps<{
|
||||
data: any[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PubBaseDataTable
|
||||
:rows="data"
|
||||
:cols="cols"
|
||||
:header="header"
|
||||
:keys="keys"
|
||||
:func-parsed="funcParsed"
|
||||
:func-html="funcHtml"
|
||||
:func-component="funcComponent"
|
||||
/>
|
||||
</template>
|
||||
@@ -4,6 +4,42 @@ import type { Summary } from '~/components/pub/base/summary-card.type'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/nav/types'
|
||||
import { CircleCheckBig, CircleDashed, CircleX, Send } from 'lucide-vue-next'
|
||||
|
||||
const data = ref([
|
||||
{
|
||||
id: 'RSC001',
|
||||
resource_type: 'Encounter',
|
||||
patient: {
|
||||
name: 'Ahmad Wepe',
|
||||
mrn: 'RM001234',
|
||||
},
|
||||
status: 2,
|
||||
updated_at: '2025-03-12',
|
||||
fhir_id: 'ENC-00123',
|
||||
},
|
||||
{
|
||||
id: 'RSC002',
|
||||
resource_type: 'Encounter',
|
||||
patient: {
|
||||
name: 'Siti Aminah',
|
||||
mrn: 'RM001235',
|
||||
},
|
||||
status: 1,
|
||||
updated_at: '2025-03-10',
|
||||
fhir_id: 'ENC-001235',
|
||||
},
|
||||
{
|
||||
id: 'RSC003',
|
||||
resource_type: 'Encounter',
|
||||
patient: {
|
||||
name: 'Budi Antono',
|
||||
mrn: 'RM001236',
|
||||
},
|
||||
status: 0,
|
||||
updated_at: '2025-03-11',
|
||||
fhir_id: 'ENC-001236',
|
||||
},
|
||||
])
|
||||
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
// open filter modal
|
||||
@@ -16,12 +52,16 @@ const refSearchNav: RefSearchNav = {
|
||||
},
|
||||
}
|
||||
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
|
||||
// Loading state management
|
||||
const isLoading = reactive({
|
||||
satusehatConn: true,
|
||||
})
|
||||
|
||||
const hreaderPrep: HeaderPrep = {
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'SATUSEHAT Integration',
|
||||
icon: 'i-lucide-box',
|
||||
addNav: {
|
||||
@@ -74,7 +114,7 @@ const summaryData: Summary[] = [
|
||||
|
||||
async function callSatuSehat() {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
service.status = 'connected'
|
||||
// service.status = 'error'
|
||||
service.sessionActive = true
|
||||
@@ -87,10 +127,14 @@ async function callSatuSehat() {
|
||||
onMounted(() => {
|
||||
callSatuSehat()
|
||||
})
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PubNavHeaderPrep :prep="hreaderPrep" :ref-search-nav="refSearchNav" />
|
||||
<PubNavHeaderPrep :prep="headerPrep" :ref-search-nav="refSearchNav" />
|
||||
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
||||
<PubBaseServiceStatus v-bind="service" />
|
||||
<div class="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
|
||||
@@ -101,5 +145,6 @@ onMounted(() => {
|
||||
<PubBaseSummaryCard v-for="card in summaryData" :key="card.title" :stat="card" />
|
||||
</template>
|
||||
</div>
|
||||
<AppSatusehatList v-if="!isLoading.satusehatConn" :data="data" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,13 +14,11 @@ defineProps<{
|
||||
|
||||
<template>
|
||||
<Table>
|
||||
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead
|
||||
v-for="(h, idx) in header[0]"
|
||||
:key="`head-${idx}`"
|
||||
:style="{ width: cols[idx]?.width ? `${cols[idx].width}px` : undefined }"
|
||||
>
|
||||
<TableHead v-for="(h, idx) in header[0]" :key="`head-${idx}`"
|
||||
:style="{ width: cols[idx]?.width ? `${cols[idx].width}px` : undefined }">
|
||||
{{ h.label }}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
@@ -30,11 +28,8 @@ defineProps<{
|
||||
<TableRow v-for="(row, rowIndex) in rows" :key="`row-${rowIndex}`">
|
||||
<TableCell v-for="(key, cellIndex) in keys" :key="`cell-${rowIndex}-${cellIndex}`">
|
||||
<!-- If funcComponent has a renderer -->
|
||||
<component
|
||||
:is="funcComponent[key](row, rowIndex).component"
|
||||
v-if="funcComponent[key]"
|
||||
v-bind="funcComponent[key](row, rowIndex)"
|
||||
/>
|
||||
<component :is="funcComponent[key](row, rowIndex).component" v-if="funcComponent[key]"
|
||||
v-bind="funcComponent[key](row, rowIndex)" />
|
||||
<!-- If funcParsed or funcHtml returns a value -->
|
||||
<template v-else>
|
||||
<!-- Use v-html for funcHtml to render HTML content -->
|
||||
|
||||
Reference in New Issue
Block a user