Merge branch 'dev' into feat/kfr-kemoterapi-174

This commit is contained in:
2025-12-01 20:47:45 +07:00
161 changed files with 5956 additions and 2860 deletions
+146
View File
@@ -0,0 +1,146 @@
<script setup lang="ts">
import Block from '~/components/pub/my-ui/doc-entry/block.vue'
import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
import Field from '~/components/pub/my-ui/doc-entry/field.vue'
import Label from '~/components/pub/my-ui/doc-entry/label.vue'
// Helpers
import type z from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { genBase } from '~/models/_base'
const props = defineProps<{
modelValue: any
schema: z.ZodSchema<any>
excludeFields?: string[]
isReadonly?: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', val: any): void
(e: 'submit', val: any): void
}>()
// Setup form
const {
validate: _validate,
defineField,
handleSubmit,
errors,
values,
} = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: props.modelValue,
})
watch(values, (val) => emit('update:modelValue', val), { deep: true })
const [subjective, subjectiveAttrs] = defineField('subjective')
const [objective, objectiveAttrs] = defineField('objective')
const [assesment, assesmentAttrs] = defineField('assesment')
const [plan, planAttrs] = defineField('plan')
const [review, reviewAttrs] = defineField('review')
const validate = async () => {
const result = await _validate()
console.log('Component validate() result:', result)
return {
valid: true,
data: result.values,
errors: result.errors,
}
}
defineExpose({ validate })
</script>
<template>
<form id="entry-form">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="my-2">
<h1 class="font-semibold">Data Petugas</h1>
</div>
<div class="my-2 rounded-md border border-slate-300 p-4">
<Block :colCount="2">
<Cell>
<Label dynamic>PPA</Label>
<Field>
<Input disabled />
</Field>
</Cell>
<Cell>
<Label dynamic>Nama PPA</Label>
<Field>
<Input disabled />
</Field>
</Cell>
</Block>
</div>
<div class="my-2">
<h1 class="font-semibold">Data S.O.A.P</h1>
</div>
<div class="my-2 rounded-md border border-slate-300 p-4">
<Block :colCount="2">
<Cell>
<Label dynamic>Subjektif</Label>
<Field>
<Textarea
v-model="subjective"
v-bind="subjectiveAttrs"
/>
</Field>
</Cell>
<Cell>
<Label dynamic>Objektif</Label>
<Field>
<Textarea
v-model="objective"
v-bind="objectiveAttrs"
/>
</Field>
</Cell>
</Block>
<Block :colCount="2">
<Cell>
<Label dynamic>Assesmen</Label>
<Field>
<Textarea
v-model="assesment"
v-bind="assesmentAttrs"
/>
</Field>
</Cell>
<Cell>
<Label dynamic>Plan</Label>
<Field>
<Textarea
v-model="plan"
v-bind="planAttrs"
/>
</Field>
</Cell>
</Block>
<Block>
<Cell>
<Label dynamic>Review</Label>
<Field>
<Textarea
v-model="review"
v-bind="reviewAttrs"
/>
</Field>
</Cell>
</Block>
</div>
<Separator class="mt-8" />
</div>
</form>
</template>
+56
View File
@@ -0,0 +1,56 @@
import type { Config, RecComponent, RecStrFuncComponent, RecStrFuncUnknown } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
import type { GeneralConsent } from '~/models/general-consent'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
export const config: Config = {
cols: [{ width: 100 }, {}, {}, {}, { width: 50 }],
headers: [
[
{ label: 'Tanggal' },
{ label: 'PPA' },
{ label: 'Hasil' },
{ label: 'Review & Verifikasi' },
{ label: 'Status' },
{ label: 'Aksi' },
],
],
keys: ['date', 'ppa', 'result', 'review', 'status', 'action'],
delKeyNames: [
{ key: 'data', label: 'Tanggal' },
{ key: 'dstDoctor.name', label: 'Dokter' },
],
parses: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
props: {
size: 'sm',
},
}
return res
},
date(rec) {
const recX = rec as GeneralConsent
return recX.date?.substring(0, 10) || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
props: {
size: 'sm',
},
}
return res
},
} as RecStrFuncComponent,
htmls: {} as RecStrFuncUnknown,
}
+34
View File
@@ -0,0 +1,34 @@
<script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
import { config } from './list.cfg'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
const props = defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<!-- FIXME: pindahkan ke content/division/list.vue -->
<PaginationView
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
</template>
@@ -0,0 +1,138 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from '~/components/pub/my-ui/data/types'
import { ActionEvents } from '~/components/pub/my-ui/data/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')!
const activeServicePosition = inject<Ref<string>>('activeServicePosition')! // previously: activePosition
const activeKey = ref<string | null>(null)
const linkItemsFiltered = ref<LinkItem[]>([])
const baseLinkItems: LinkItem[] = [
{
label: 'Detail',
value: 'detail',
onClick: () => {
proceedItem(ActionEvents.showDetail)
},
icon: 'i-lucide-eye',
},
]
const medicalLinkItems: LinkItem[] = [
{
label: 'Process',
value: 'process',
onClick: () => {
proceedItem(ActionEvents.showProcess)
},
icon: 'i-lucide-shuffle',
},
]
const regLinkItems: LinkItem[] = [
{
label: 'Print',
value: 'print',
onClick: () => {
proceedItem(ActionEvents.showPrint)
},
icon: 'i-lucide-printer',
},
{
label: 'Batalkan',
value: 'cancel',
onClick: () => {
proceedItem(ActionEvents.showCancel)
},
icon: 'i-lucide-circle-x',
},
{
label: 'Hapus',
value: 'remove',
onClick: () => {
proceedItem(ActionEvents.showConfirmDelete)
},
icon: 'i-lucide-trash',
},
]
const voidLinkItems: LinkItem[] = [
{
label: 'Nothing',
value: 'nothing',
icon: 'i-lucide-file',
},
]
linkItemsFiltered.value = [...baseLinkItems]
getLinks()
watch(activeServicePosition, () => {
getLinks()
})
function proceedItem(action: string) {
recId.value = props.rec.id || 0
recItem.value = props.rec
recAction.value = action
}
function getLinks() {
switch (activeServicePosition.value) {
case 'medical':
linkItemsFiltered.value = [...baseLinkItems, ...medicalLinkItems]
break
case 'registration':
linkItemsFiltered.value = [...baseLinkItems, ...regLinkItems]
case 'unit|resp':
linkItemsFiltered.value = [...baseLinkItems]
break
default:
linkItemsFiltered.value = voidLinkItems
break
}
}
</script>
<template>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
>
<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 border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
align="end"
>
<DropdownMenuGroup>
<DropdownMenuItem
v-for="item in linkItemsFiltered"
:key="item.label"
class="hover:bg-gray-100 dark:hover:bg-slate-700"
@click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
>
<Icon :name="item.icon ?? ''" />
<span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>
+9 -3
View File
@@ -20,6 +20,7 @@ import type { TreeItem } from '~/components/pub/my-ui/select-tree/type'
// Helpers
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { refDebounced } from '@vueuse/core'
const props = defineProps<{
isLoading?: boolean
@@ -92,8 +93,9 @@ watch(subSpecialistId, async (newValue) => {
}
})
// Watch SEP number changes to notify parent
watch(sepNumber, (newValue) => {
// Debounced SEP number watcher: emit change only after user stops typing
const debouncedSepNumber = refDebounced(sepNumber, 500)
watch(debouncedSepNumber, (newValue) => {
emit('event', 'sep-number-changed', newValue)
})
@@ -452,7 +454,11 @@ defineExpose({
name="i-lucide-loader-2"
class="h-4 w-4 animate-spin"
/>
<span v-else>+</span>
<Icon
v-else
name="i-lucide-plus"
class="h-4 w-4"
/>
</Button>
<Button
v-else
+123
View File
@@ -0,0 +1,123 @@
<script setup lang="ts">
import { Calendar as CalendarIcon, Filter as FilterIcon, Search } from 'lucide-vue-next'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { DateRange } from 'radix-vue'
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
import { cn } from '~/lib/utils'
import type { RefExportNav, RefSearchNav } from '~/components/pub/my-ui/data/types'
const props = defineProps<{
refSearchNav?: RefSearchNav
enableExport?: boolean
refExportNav?: RefExportNav
onFilterClick?: () => void
onExportPdf?: () => void
onExportExcel?: () => void
onExportCsv?: () => void
}>()
// function emitSearchNavClick() {
// props.refSearchNav?.onClick()
// }
//
// function onInput(event: Event) {
// props.refSearchNav?.onInput((event.target as HTMLInputElement).value)
// }
//
// function btnClick() {
// props.prep?.addNav?.onClick?.()
// }
const searchQuery = ref('')
const dateRange = ref<{ from: Date | null; to: Date | null }>({
from: new Date(),
to: new Date(),
})
const df = new DateFormatter('en-US', {
dateStyle: 'medium',
})
// Get current date
const today = new Date()
const todayCalendar = new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate())
// Get date 1 month ago
const oneMonthAgo = new Date(today)
oneMonthAgo.setMonth(today.getMonth() - 1)
const oneMonthAgoCalendar = new CalendarDate(oneMonthAgo.getFullYear(), oneMonthAgo.getMonth() + 1, oneMonthAgo.getDate())
const value = ref({
start: oneMonthAgoCalendar,
end: todayCalendar,
}) as Ref<DateRange>
// function onFilterClick() {
// console.log('Search:', searchQuery.value)
// console.log('Date Range:', dateRange.value)
// props.refSearchNav?.onClick()
// }
</script>
<template>
<div class="relative w-64">
<Search class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-gray-400" />
<Input v-model="searchQuery" type="text" placeholder="Cari Nama /No.RM" class="pl-9" />
</div>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn('w-[200px] justify-start text-left font-normal', !value && '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>
<template v-else>
{{ df.format(value.start.toDate(getLocalTimeZone())) }}
</template>
</template>
<template v-else> Pick a date </template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<RangeCalendar
v-model="value"
initial-focus
:number-of-months="2"
@update:start-value="(startDate) => (value.start = startDate)"
/>
</PopoverContent>
</Popover>
<Button variant="outline" class="border-orange-500 text-orange-600 hover:bg-orange-50" @click="onFilterClick">
<FilterIcon class="mr-2 size-4" />
Filter
</Button>
<DropdownMenu v-show="props.enableExport">
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto border-orange-500 text-orange-600 hover:bg-orange-50">
<Icon name="i-lucide-download" class="h-4 w-4" />
Ekspor
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem @click="onExportPdf">
Ekspor PDF
</DropdownMenuItem>
<DropdownMenuItem @click="onExportCsv">
Ekspor CSV
</DropdownMenuItem>
<DropdownMenuItem @click="onExportExcel">
Ekspor Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
@@ -0,0 +1,133 @@
<script setup lang="ts">
// Components
import { Button } from '~/components/pub/ui/button'
const route = useRoute()
const router = useRouter()
// Track active menu item from query param
const activeMenu = computed(() => route.query.menu as string || '')
interface ButtonItems {
label: string
icon: string
value: string
type: 'icon' | 'image'
}
const itemsOne: ButtonItems[] = [
{
label: 'Data Pendaftaran',
icon: 'i-lucide-file',
value: 'register',
type: 'icon',
},
{
label: 'Status Pembayaran',
icon: 'i-lucide-banknote-arrow-down',
value: 'status',
type: 'icon',
},
{
label: 'Riwayat Pasien',
icon: 'i-lucide-history',
value: 'history',
type: 'icon',
},
{
label: 'Penunjang',
icon: 'i-lucide-library-big',
value: 'support',
type: 'icon',
},
{
label: 'Resep',
icon: 'i-lucide-pill',
value: 'receipt',
type: 'icon',
},
{
label: 'DPJP',
icon: 'i-lucide-stethoscope',
value: 'doctor',
type: 'icon',
},
{
label: 'I-Care BPJS',
icon: '/bpjs.png',
value: 'bpjs',
type: 'image',
},
{
label: 'File SEP',
icon: 'i-lucide-file',
value: 'sep',
type: 'icon',
},
]
const itemsTwo: ButtonItems[] = [
{
label: 'Tarif Tindakan',
icon: 'i-lucide-banknote-arrow-down',
value: 'price-list',
type: 'icon',
},
{
label: 'Tarif Tindakan Paket',
icon: 'i-lucide-banknote-arrow-down',
value: 'price-list-package',
type: 'icon',
},
]
function handleClick(value: string) {
router.replace({ path: route.path, query: { menu: value } })
}
</script>
<template>
<div class="my-4">
<h2 class="mb-4 text-base font-semibold md:text-lg 2xl:text-xl">History Pasien:</h2>
<div class="flex flex-wrap gap-2 mb-2">
<Button
v-for="item in itemsOne"
:key="item.value"
:class="[
'flex items-center gap-2 rounded-lg border px-3 py-1 text-sm font-medium transition-colors',
activeMenu === item.value
? 'border-orange-300 bg-orange-400 text-white'
: 'border-orange-300 bg-white text-orange-500 hover:bg-orange-400 hover:text-white'
]"
@click="handleClick(item.value)"
>
<Icon v-if="item.type === 'icon'"
:name="item.icon"
class="h-4 w-4"
/>
<img v-else-if="item.type === 'image'" :src="item.icon" class="h-[24px] w-[24px] object-contain" />
{{ item.label }}
</Button>
</div>
<h2 class="mb-4 text-base font-semibold md:text-lg 2xl:text-xl">Billing Pasien:</h2>
<div class="flex flex-wrap gap-2 mb-2">
<Button
v-for="item in itemsTwo"
:key="item.value"
:class="[
'flex items-center gap-2 rounded-lg border px-3 py-1 text-sm font-medium transition-colors',
activeMenu === item.value
? 'border-orange-300 bg-orange-400 text-white'
: 'border-orange-300 bg-white text-orange-500 hover:bg-orange-400 hover:text-white'
]"
@click="handleClick(item.value)"
>
<Icon
:name="item.icon"
class="h-4 w-4"
/>
{{ item.label }}
</Button>
</div>
</div>
</template>
+1 -1
View File
@@ -6,7 +6,7 @@ import { getAge } from '~/lib/date'
type SmallDetailDto = Encounter
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-pdud.vue'))
const action = defineAsyncComponent(() => import('./dropdown-action.vue'))
const statusBadge = defineAsyncComponent(() => import('./status-badge.vue'))
export const config: Config = {
+4 -3
View File
@@ -1,14 +1,15 @@
<script setup lang="ts">
import DataTable from '~/components/pub/my-ui/data-table/data-table.vue'
import { config } from './list.cfg'
const props = defineProps<{
data: any[]
data: any[],
}>()
</script>
<template>
<PubMyUiDataTable
<DataTable
v-bind="config"
:rows="data"
:rows="props.data"
/>
</template>
@@ -0,0 +1,28 @@
<script setup lang="ts">
// Types
import type { Encounter } from '~/models/encounter'
// Components
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '~/components/pub/ui/accordion'
import EncounterQuickInfoFull from '~/components/app/encounter/quick-info-full.vue'
const props = defineProps<{
data: Encounter
}>()
</script>
<template>
<div class="w-full rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
<!-- Data Pasien -->
<Accordion type="single" defaultValue="item-patient" collapsible>
<AccordionItem value="item-patient" class="border-none">
<AccordionTrigger class="focus:outline-none focus:ring-0">
<h2 class="mb-4 text-base font-semibold md:text-lg 2xl:text-xl">Data Pasien:</h2>
</AccordionTrigger>
<AccordionContent>
<EncounterQuickInfoFull :data="props.data" :is-grid="true" />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</template>
@@ -0,0 +1,18 @@
<script setup lang="ts">
// Types
import type { Encounter } from '~/models/encounter'
// Components
import EncounterQuickInfoFull from '~/components/app/encounter/quick-info-full.vue'
const props = defineProps<{
data: Encounter
}>()
</script>
<template>
<div class="w-full rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
<h2 class="mb-4 text-base font-semibold md:text-lg 2xl:text-xl">Data Pasien:</h2>
<EncounterQuickInfoFull :data="props.data" :is-grid="false" />
</div>
</template>
@@ -0,0 +1,247 @@
<script setup lang="ts">
// Helpers
import { format, parseISO } from 'date-fns'
import { id as localeID } from 'date-fns/locale'
import { getAge } from '~/lib/date'
import { cn } from "~/lib/utils"
// Types
import type { Encounter } from '~/models/encounter'
import { paymentTypes } from '~/lib/constants.vclaim'
const props = defineProps<{
data: Encounter
isGrid?: boolean
}>()
const isGrid = props.isGrid !== undefined ? props.isGrid : true
// Address
const address = computed(() => {
if (props.data.patient.person.addresses && props.data.patient.person.addresses.length > 0) {
return props.data.patient.person.addresses.map((a) => a.address).join(', ')
}
return '-'
})
// DPJP
const dpjp = computed(() => {
if (props.data.responsible_doctor) {
const dp = props.data.responsible_doctor.employee.person
return `${dp.frontTitle || ''} ${dp.name || ''} ${dp.endTitle || ''}`.trim()
} else if (props.data.appointment_doctor) {
const dp = props.data.appointment_doctor.employee.person
return `${dp.frontTitle || ''} ${dp.name || ''} ${dp.endTitle || ''}`.trim()
}
return '-'
})
// Tgl. Lahir dengan Umur
const birthDateFormatted = computed(() => {
if (!props.data.patient.person.birthDate) {
return '-'
}
try {
const ageData = getAge(props.data.patient.person.birthDate)
const ageYears = ageData.extFormat.split(' ')[0] || '0'
return `${ageData.idFormat} (${ageYears} Tahun)`
} catch {
return '-'
}
})
// Tgl. Masuk RS dengan waktu
const registeredDateFormatted = computed(() => {
const dateStr = props.data.registeredAt || props.data.visitDate
if (!dateStr) {
return '-'
}
try {
const date = parseISO(dateStr)
return format(date, 'dd MMMM yyyy HH:mm', { locale: localeID })
} catch {
return '-'
}
})
// Jns. Kelamin
const genderLabel = computed(() => {
const code = props.data.patient.person.gender_code
if (!code) return '-'
// Map common gender codes
if (code === 'M' || code === 'male' || code.toLowerCase() === 'l') {
return 'Laki-laki'
} else if (code === 'F' || code === 'female' || code.toLowerCase() === 'p') {
return 'Perempuan'
}
return code
})
// Jns. Pembayaran
const paymentTypeLabel = computed(() => {
const code = props.data.paymentMethod_code
if (!code) return '-'
// Map payment method codes
if (code === 'insurance') {
return 'JKN'
} else if (code === 'jkn') {
return 'JKN'
} else if (code === 'jkmm') {
return 'JKMM'
} else if (code === 'spm') {
return 'SPM'
} else if (code === 'pks') {
return 'PKS'
}
// Try to get from paymentTypes constant
if (paymentTypes[code]) {
return paymentTypes[code].split(' ')[0] // Get first part (e.g., "JKN" from "JKN (Jaminan...)")
}
return code
})
// No. Billing - try to get from trx_number or other billing fields
const billingNumber = computed(() => {
// Check if encounter has payment data with trx_number
if ((props.data as any).trx_number) {
return (props.data as any).trx_number
}
// Check if encounter has payment relation
if ((props.data as any).payment?.trx_number) {
return (props.data as any).payment.trx_number
}
return '-'
})
// Nama Ruang - from unit name or room relation
const roomName = computed(() => {
if (props.data.unit?.name) {
return props.data.unit.name
}
// Check if there's a room relation
if ((props.data as any).room?.name) {
return (props.data as any).room.name
}
return '-'
})
// No Bed - from bed relation or field
const bedNumber = computed(() => {
// Check if encounter has bed data
if ((props.data as any).bed?.number) {
return (props.data as any).bed.number
}
if ((props.data as any).bed_number) {
return (props.data as any).bed_number
}
return '-'
})
</script>
<template>
<h2 class="mb-4 font-semibold md:text-base 2xl:text-lg">Data Pasien:</h2>
<!-- 4 Column Grid Layout -->
<div :class="cn('grid grid-cols-4 gap-4', isGrid && 'sm:grid-cols-4')">
<!-- No. RM -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">No. RM</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ data.patient.number || '-' }}
</p>
</div>
<!-- Tgl. Lahir -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Tgl. Lahir</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ birthDateFormatted }}
</p>
</div>
<!-- Jns. Pembayaran -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Cara Bayar</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ paymentTypeLabel }}
</p>
</div>
<!-- No Bed -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">No Bed</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ bedNumber }}
</p>
</div>
<!-- Nama Pasien -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">No. Bed</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ data.patient.person.name || '-' }}
</p>
</div>
<!-- Tgl. Masuk RS -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Tgl. Masuk RS</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ registeredDateFormatted }}
</p>
</div>
<!-- No. Billing -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">No. Billing</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ billingNumber }}
</p>
</div>
<!-- DPJP -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">DPJP</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ dpjp }}
</p>
</div>
<!-- Alamat -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Alamat</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ address }}
</p>
</div>
<!-- Jns. Kelamin -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Jns. Kelamin</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ genderLabel }}
</p>
</div>
<!-- Nama Ruang -->
<div class="flex gap-1">
<label class="w-20 2xl:w-24 flex-none text-sm font-semibold text-gray-700 dark:text-gray-300">Nama Ruang</label>
<label class="w-2 flex-none">:</label>
<p class="flex-1 text-sm text-gray-900 dark:text-gray-100">
{{ roomName }}
</p>
</div>
</div>
</template>
+30 -25
View File
@@ -1,52 +1,52 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
import { genderCodes } from '~/const/key-val/person';
import type { Encounter } from '~/models/encounter'
const props = defineProps<{
data: Encounter
}>()
let address = ''
let address = ref('')
if (props.data.patient.person.addresses) {
address = props.data.patient.person.addresses.map((a) => a.address).join(', ')
address.value = props.data.patient.person.addresses.map((a) => a.address).join(', ')
}
let dpjp = ''
let dpjp = ref('')
if (props.data.responsible_doctor) {
const dp = props.data.responsible_doctor.employee.person
dpjp = `${dp.frontTitle} ${dp.name} ${dp.endTitle}`
dpjp.value = `${dp.frontTitle} ${dp.name} ${dp.endTitle}`
} else if (props.data.appointment_doctor) {
dpjp = props.data.appointment_doctor.employee.person.name
dpjp.value = props.data.appointment_doctor.employee.person.name
}
</script>
<template>
<div class="w-full rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
<div class="w-full">
<!-- Data Pasien -->
<h2 class="mb-2 font-semibold md:text-base 2xl:text-lg">
{{ data.patient.person.name }} - {{ data.patient.number }}
</h2>
<div class="grid grid-cols-3">
<div>
<DE.Block
mode="preview"
labelSize="large"
>
<DE.Block mode="preview">
<DE.Cell>
<DE.Label class="font-semibold">No. RM</DE.Label>
<DE.Colon />
<DE.Field>
{{ data.patient.person.birthDate?.substring(0, 10) }}
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label class="font-semibold">Jenis Kelamin</DE.Label>
<DE.Label class="font-semibold">Jns. Kelamin</DE.Label>
<DE.Colon />
<DE.Field>
{{ data.patient.person.gender_code }}
{{ genderCodes[data.patient.person.gender_code as keyof typeof genderCodes] }}
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label class="font-semibold">Alamat</DE.Label>
<DE.Colon />
<DE.Field>
<div v-html="address"></div>
</DE.Field>
@@ -54,35 +54,39 @@ if (props.data.responsible_doctor) {
</DE.Block>
</div>
<div>
<DE.Block
mode="preview"
labelSize="large"
>
<DE.Block mode="preview">
<DE.Cell>
<DE.Label class="font-semibold">Tgl. Kunjungan</DE.Label>
<DE.Label position="dynamic" class="font-semibold">Tgl. Masuk</DE.Label>
<DE.Colon />
<DE.Field>
{{ data.visitDate.substring(0, 10) }}
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label class="font-semibold">Klinik</DE.Label>
<DE.Label position="dynamic" class="font-semibold">Poliklinik</DE.Label>
<DE.Colon />
<DE.Field>
{{ data.unit?.name }}
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label class="font-semibold">DPJP</DE.Label>
<DE.Label position="dynamic" class="font-semibold">Klinik</DE.Label>
<DE.Colon />
<DE.Field>
{{ dpjp }}
{{ data.unit?.name }}
</DE.Field>
</DE.Cell>
</DE.Block>
</div>
<div>
<DE.Block
mode="preview"
labelSize="large"
>
<DE.Block mode="preview">
<DE.Cell>
<DE.Label position="dynamic" class="font-semibold">DPJP</DE.Label>
<DE.Colon />
<DE.Field>
{{ dpjp }}
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label
position="dynamic"
@@ -90,6 +94,7 @@ if (props.data.responsible_doctor) {
>
Billing
</DE.Label>
<DE.Colon class="pt-1" />
<DE.Field class="text-base 2xl:text-lg">
Rp. 000.000
<!-- {{ data }} -->
@@ -0,0 +1,130 @@
<script setup lang="ts">
// Components
import { Button } from '~/components/pub/ui/button'
const route = useRoute()
const router = useRouter()
// Track active menu item from query param
const activeMenu = computed(() => route.query.menu as string || '')
interface ButtonItems {
label: string
icon: string
value: string
type: 'icon' | 'image'
}
const itemsOne: ButtonItems[] = [
{
label: 'Data Pendaftaran',
icon: 'i-lucide-file',
value: 'register',
type: 'icon',
},
{
label: 'Status Pembayaran',
icon: 'i-lucide-banknote-arrow-down',
value: 'status',
type: 'icon',
},
{
label: 'Riwayat Pasien',
icon: 'i-lucide-history',
value: 'history',
type: 'icon',
},
{
label: 'Penunjang',
icon: 'i-lucide-library-big',
value: 'support',
type: 'icon',
},
{
label: 'Resep',
icon: 'i-lucide-pill',
value: 'receipt',
type: 'icon',
},
{
label: 'DPJP',
icon: 'i-lucide-stethoscope',
value: 'doctor',
type: 'icon',
},
{
label: 'I-Care BPJS',
icon: '/bpjs.png',
value: 'bpjs',
type: 'image',
},
{
label: 'File SEP',
icon: 'i-lucide-file',
value: 'sep',
type: 'icon',
},
]
const itemsTwo: ButtonItems[] = [
{
label: 'Tarif Tindakan',
icon: 'i-lucide-banknote-arrow-down',
value: 'price-list',
type: 'icon',
},
{
label: 'Tarif Tindakan Paket',
icon: 'i-lucide-banknote-arrow-down',
value: 'price-list-package',
type: 'icon',
},
]
function handleClick(value: string) {
router.replace({ path: route.path, query: { menu: value } })
}
</script>
<template>
<h2 class="mb-4 font-semibold md:text-base 2xl:text-lg">Menu Cepat:</h2>
<div class="flex flex-wrap gap-2 mb-2">
<Button
v-for="item in itemsOne"
:key="item.value"
:class="[
'flex items-center gap-2 rounded-lg border px-3 py-1 text-sm font-medium transition-colors',
activeMenu === item.value
? 'border-orange-300 bg-orange-400 text-white'
: 'border-orange-300 bg-white text-orange-500 hover:bg-orange-400 hover:text-white'
]"
@click="handleClick(item.value)"
>
<Icon v-if="item.type === 'icon'"
:name="item.icon"
class="h-4 w-4"
/>
<img v-else-if="item.type === 'image'" :src="item.icon" class="h-[24px] w-[24px] object-contain" />
{{ item.label }}
</Button>
</div>
<div class="flex flex-wrap gap-2 mb-2">
<Button
v-for="item in itemsTwo"
:key="item.value"
:class="[
'flex items-center gap-2 rounded-lg border px-3 py-1 text-sm font-medium transition-colors',
activeMenu === item.value
? 'border-orange-300 bg-orange-400 text-white'
: 'border-orange-300 bg-white text-orange-500 hover:bg-orange-400 hover:text-white'
]"
@click="handleClick(item.value)"
>
<Icon
:name="item.icon"
class="h-4 w-4"
/>
{{ item.label }}
</Button>
</div>
</template>
@@ -12,7 +12,7 @@ const statusCodeColors: Record<string, Variants> = {
review: 'fresh',
process: 'fresh',
done: 'positive',
canceled: 'destructive',
cancel: 'destructive',
rejected: 'destructive',
skiped: 'negative',
}
+7
View File
@@ -143,12 +143,16 @@ watch(props, (value) => {
nationalId.value = objects?.nationalIdentity || '-'
medicalRecordNumber.value = objects?.medicalRecordNumber || '-'
patientName.value = objects?.patientName || '-'
phoneNumber.value = objects?.phoneNumber || '-'
if (objects?.sepType === 'internal') {
admissionType.value = '4'
}
if (objects?.sepType === 'external') {
admissionType.value = '1'
}
if (objects?.diagnoseLabel) {
initialDiagnosis.value = objects?.diagnoseLabel
}
isDateReload.value = true
setTimeout(() => {
if (objects?.letterDate) {
@@ -176,6 +180,9 @@ onMounted(() => {
if (!isService.value) {
serviceType.value = '2'
}
if (!admissionType.value) {
admissionType.value = '1'
}
})
</script>
+2 -1
View File
@@ -11,7 +11,8 @@ const props = defineProps<{
<template>
<PubMyUiDataTable
v-bind="config"
:rows="props.data"
v-bind="config"
v-on="$attrs"
/>
</template>
+6 -2
View File
@@ -3,7 +3,7 @@ import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
export const config: Config = {
cols: [{}, {}, {}, { width: 100 }, { width: 120 }, {}, {}, {}, { width: 100 }, { width: 100 }, {}, { width: 50 }],
@@ -20,7 +20,7 @@ export const config: Config = {
],
],
keys: ['time', 'employee_id', 'main_complaint', 'encounter_id', 'diagnose', 'status', 'action'],
keys: ['time', 'employee_id', 'main_complaint', 'encounter', 'diagnose', 'status', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
@@ -44,6 +44,10 @@ export const config: Config = {
return '-'
}
},
encounter(rec: any) {
const data = rec?.encounter ?? {}
return data?.class_code || '-'
},
diagnose(rec: any) {
const { value } = rec ?? {}