feat (patient): implement patient list and entry form

This commit is contained in:
Abizrh
2025-08-12 17:01:50 +07:00
parent 500cdb6a21
commit 7ae9a9adfe
9 changed files with 342 additions and 13 deletions
+2 -3
View File
@@ -8,8 +8,7 @@ import Label from '~/components/pub/form/label.vue'
<template>
<form id="entry-form">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<!-- <i class="bi bi-file-earmark-person"></i> -->
<Icon name="i-lucide-user" />
<Icon name="i-lucide-user" class="me-2" />
<span class="font-semibold">Tambah</span> Pasien
</div>
@@ -32,7 +31,7 @@ import Label from '~/components/pub/form/label.vue'
</div>
</div>
<div class="my-2 flex justify-end py-2">
<PubNavFooterCs />
<PubNavFooterCsd />
</div>
</form>
</template>
+118
View File
@@ -0,0 +1,118 @@
import type {
Col,
KeyLabel,
RecComponent,
RecStrFuncComponent,
RecStrFuncUnknown,
Th,
} from '~/components/pub/nav/types'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/nav/dropdown-action-dud.vue'))
export const cols: Col[] = [
{},
{},
{},
{ width: 100 },
{ width: 120 },
{},
{},
{},
{ width: 100 },
{ width: 100 },
{},
{ width: 50 },
]
export const header: Th[][] = [
[
{ label: 'Nama' },
{ label: 'Rekam Medis' },
{ label: 'KTP' },
{ label: 'Tgl Lahir' },
{ label: 'Umur' },
{ label: 'JK' },
{ label: 'Pendidikan' },
{ label: 'Status' },
{ label: '' },
],
]
export const keys = [
'name',
'medicalRecord_number',
'identity_number',
'birth_date',
'patient_age',
'gender',
'education',
'status',
'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 || ''}`
},
identity_number: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (recX.identity_number?.substring(0, 5) === 'BLANK') {
return '(TANPA NIK)'
}
return recX.identity_number
},
birth_date: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (typeof recX.birth_date == 'object' && recX.birth_date) {
return (recX.birth_date as Date).toLocaleDateString()
} else if (typeof recX.birth_date == 'string') {
return (recX.birth_date as string).substring(0, 10)
}
return recX.birth_date
},
patient_age: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.birth_date?.split('T')[0]
},
gender: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (typeof recX?.gender_code !== 'number' && recX?.gender_code !== '') {
return 'Tidak Diketahui'
}
return recX.gender_code
},
education: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (typeof recX.education_code == 'number' && recX.education_code >= 0) {
return recX.education_code
} else if (typeof recX.education_code) {
return recX.education_code
}
return '-'
},
}
export const funcComponent: RecStrFuncComponent = {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
}
export const funcHtml: RecStrFuncUnknown = {
patient_address(_rec) {
return '-'
},
}
+19
View File
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { cols, header, keys, funcParsed, funcHtml, funcComponent } from './list-cfg.ts'
defineProps<{
data: any[]
}>()
</script>
<template>
<PubNavDataTable
:rows="data"
:cols="cols"
:header="header"
:keys="keys"
:func-parsed="funcParsed"
:func-html="funcHtml"
:func-component="funcComponent"
/>
</template>
+18 -2
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
const data = ref([])
const refSearchNav = {
onClick: () => {
// open filter modal
@@ -11,6 +13,10 @@ const refSearchNav = {
},
}
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const hreaderPrep: HeaderPrep = {
title: 'Pasien',
icon: 'bi bi-journal-check',
@@ -22,15 +28,25 @@ const hreaderPrep: HeaderPrep = {
// NOTE: example api
async function getPatientList() {
const { data } = await xfetch('/api/v1/patient')
console.log('data patient', data)
const resp = await xfetch('/api/v1/patient')
console.log('data patient', resp)
if (resp.success) {
data.value = (resp.body as Record<string, any>)['data']
}
}
onMounted(() => {
getPatientList()
})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
</script>
<template>
<PubNavHeaderPrep :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" icon="i-lucide-add" />
<div>
<AppPatientList :data="data" />
</div>
</template>
+46
View File
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/pub/ui/table'
defineProps<{
rows: unknown[]
cols: object
header: object[]
keys: string[]
funcParsed: object
funcHtml: object
funcComponent: object
}>()
</script>
<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 }"
>
{{ h.label }}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<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
v-if="funcComponent[key]"
:is="funcComponent[key](row, rowIndex).component"
v-bind="funcComponent[key](row, rowIndex)"
/>
<!-- If funcParsed or funcHtml returns a value -->
<template v-else>
{{ funcParsed[key]?.(row) ?? funcHtml[key]?.(row) ?? row[key] }}
</template>
</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
@@ -0,0 +1,82 @@
<script setup lang="ts">
import type { ListItemDto, LinkItem } from './types'
const props = defineProps<{
rec: ListItemDto
}>()
const recId = inject<Ref<number>>('rec_id')!
const recAction = inject<Ref<string>>('rec_action')!
const recItem = inject<Ref<any>>('rec_item')!
function detail() {
recId.value = props.rec.id || 0
recAction.value = 'showDetail'
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = 'showEdit'
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = 'showConfirmDel'
recItem.value = props.rec
}
const linkItems: LinkItem[] = [
{
label: 'Detail',
onClick: () => {
detail()
},
icon: 'i-lucide-eye',
},
{
label: 'Edit',
onClick: () => {
edit()
},
icon: 'i-lucide-pencil',
},
{
label: 'Hapus',
onClick: () => {
del()
},
icon: 'i-lucide-trash',
},
]
</script>
<template>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white"
>
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg bg-white" align="end">
<DropdownMenuGroup>
<DropdownMenuItem
v-for="item in linkItems"
:key="item.label"
v-slot="{ active }"
class="hover:bg-gray-100"
@click="item.onClick"
>
<Icon :name="item.icon" />
<span :class="active ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>
+20 -8
View File
@@ -1,12 +1,24 @@
<script setup lang="ts"></script>
<script setup lang="ts">
type ClickType = 'cancel' | 'submit'
const emit = defineEmits<{
(e: 'click', type: ClickType): void
}>()
function onClick(type: ClickType) {
emit('click', type)
}
</script>
<template>
<div class="flex justify-between px-2">
<div>
<Button variant="outline">
<Icon name="i-lucide-pencil" class="mr-1" />
Edit
</Button>
</div>
<div class="m-2 flex gap-2 px-2">
<Button class="bg-gray-400" @click="onClick('cancel')">
<Icon name="i-lucide-arrow-left" class="me-2 align-middle" />
Kembali
</Button>
<Button class="bg-primary" @click="onClick('submit')">
<Icon name="i-lucide-check" class="me-2 align-middle" />
Selesai
</Button>
</div>
</template>
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
type ClickType = 'cancel' | 'draft' | 'submit'
const emit = defineEmits<{
(e: 'click', type: ClickType): void
}>()
function onClick(type: ClickType) {
emit('click', type)
}
</script>
<template>
<div class="m-2 flex gap-2 px-2">
<Button class="bg-gray-400" @click="onClick('cancel')">
<Icon name="i-lucide-arrow-left" class="me-2 align-middle" />
Kembali
</Button>
<Button class="bg-orange-500" variant="outline" @click="onClick('draft')">
<Icon name="i-lucide-file" class="me-2" />
Draf
</Button>
<Button class="bg-primary" @click="onClick('submit')">
<Icon name="i-lucide-check" class="me-2 align-middle" />
Selesai
</Button>
</div>
</template>
+9
View File
@@ -82,3 +82,12 @@ export interface KeyNames {
key: string
label: string
}
export interface LinkItem {
label: string
icon?: string
href?: string // to cover the needs of stating full external origins full url
action?: string // for local paths
onClick?: (event: Event) => void
headerStatus?: boolean
}