feat(encounter): laporan tindakan cru

feat(treatment-report): add view mode for treatment reports

- Introduce new View component for displaying treatment report details
- Extend useQueryCRUDMode to support 'view' mode
- Update List component to handle view actions
- Implement navigation between list, entry and view modes

refactor(treatment-report): improve navigation flow between views

- Add fromView flag to track navigation origin
- Implement smart back navigation (goBack) that returns to previous view
- Update edit handlers to pass navigation context
- Clean up unused back handlers in form component

cleanup

set responsive details
This commit is contained in:
Khafid Prayoga
2025-12-02 13:20:43 +07:00
parent be1b86141f
commit dfc25f0048
10 changed files with 181 additions and 291 deletions
+81 -66
View File
@@ -59,16 +59,21 @@ function onNavigate(type: string) {
<AccordionItem value="section-1">
<AccordionTrigger>Tim Pelaksanaan Tindakan</AccordionTrigger>
<AccordionContent>
<DE.Block>
<DetailRow label="DPJP">dr. Marcell Galliard Sp.Gr</DetailRow>
<DetailRow label="Operator">Sumitro</DetailRow>
<DetailRow label="Asisten Operator">Alexis Lewis Carol</DetailRow>
<DetailRow label="Instrumentir">Mikel Arteta</DetailRow>
<DetailRow label="Tanggal Pembedahan">
{{ format(new Date(), 'd MMMM yyyy', { locale: id }) }}
</DetailRow>
<DetailRow label="Diagnosa Tindakan">{{ operatorTeam?.actionDiagnosis || '-' }}</DetailRow>
<DetailRow label="Perawat Pasca Bedah">Cak Armuji</DetailRow>
<DE.Block
:cell-flex="false"
:col-count="2"
>
<DE.Cell>
<DetailRow label="DPJP">dr. Marcell Galliard Sp.Gr</DetailRow>
<DetailRow label="Operator">Sumitro</DetailRow>
<DetailRow label="Asisten Operator">Alexis Lewis Carol</DetailRow>
<DetailRow label="Instrumentir">Mikel Arteta</DetailRow>
<DetailRow label="Tanggal Pembedahan">
{{ format(new Date(), 'd MMMM yyyy', { locale: id }) }}
</DetailRow>
<DetailRow label="Diagnosa Tindakan">{{ operatorTeam?.actionDiagnosis || '-' }}</DetailRow>
<DetailRow label="Perawat Pasca Bedah">Cak Armuji</DetailRow>
</DE.Cell>
</DE.Block>
</AccordionContent>
</AccordionItem>
@@ -76,69 +81,79 @@ function onNavigate(type: string) {
<AccordionItem value="section-2">
<AccordionTrigger>Tindakan Operatif / Non Operatif Lain</AccordionTrigger>
<AccordionContent>
<ArrangementProcedurePicker
field-name="procedures"
title="List Prosedur"
sub-title="Pilih Prosedur"
:mode="'preview'"
:sample-items="procedureSampleData"
/>
<DE.Block
:cell-flex="false"
:col-count="2"
>
<DE.Cell>
<ArrangementProcedurePicker
field-name="procedures"
title="List Prosedur"
sub-title="Pilih Prosedur"
:mode="'preview'"
:sample-items="procedureSampleData"
/>
</DE.Cell>
</DE.Block>
</AccordionContent>
</AccordionItem>
<AccordionItem value="section-3">
<AccordionTrigger>Data Pelaksanaan Tindakan</AccordionTrigger>
<AccordionContent>
<DE.Block>
<DetailRow label="Jenis Operasi">dr. Marcell Galliard Sp.Gr</DetailRow>
<DetailRow label="Kode Billing">GCASH1128190</DetailRow>
<DetailRow label="Sistem Operasi">Alexis Lewis Carol</DetailRow>
<DetailRow label="Operasi Mulai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Operasi Selesai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Lama Operasi">5 menit</DetailRow>
<DetailRow label="Pembiusan Mulai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Pembiusan Selesai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Lama Pembiusan">5 menit</DetailRow>
<DE.Block
:cell-flex="false"
:col-count="2"
>
<DE.Cell>
<DetailRow label="Jenis Operasi">dr. Marcell Galliard Sp.Gr</DetailRow>
<DetailRow label="Kode Billing">GCASH1128190</DetailRow>
<DetailRow label="Sistem Operasi">Alexis Lewis Carol</DetailRow>
<DetailRow label="Operasi Mulai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Operasi Selesai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Lama Operasi">5 menit</DetailRow>
<DetailRow label="Pembiusan Mulai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Pembiusan Selesai">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<DetailRow label="Lama Pembiusan">5 menit</DetailRow>
<DetailRow label="PRC">300 CC</DetailRow>
<DetailRow label="FPP">-</DetailRow>
<DetailRow label="WB">-</DetailRow>
<DetailRow label="TC">-</DetailRow>
<DetailRow label="Merk">-</DetailRow>
<DetailRow label="Nama Implant">-</DetailRow>
<DetailRow label="Sticker / Nomor Register Implant">-</DetailRow>
<DetailRow label="Nama Pendamping Implant">-</DetailRow>
</DE.Block>
<DE.Block>
<DetailRow label="Jenis Pembedahan">Bersih</DetailRow>
<DetailRow label="Operasi ke">1 (Satu)</DetailRow>
<DetailRow label="Keterangan Lahir">Lahir Hidup</DetailRow>
<DetailRow label="Ket. Tempat Lahir">RSSA</DetailRow>
<DetailRow label="Berat Badan">18 gram</DetailRow>
<DetailRow label="Ket. Saat Lahir">Normal dan sehat</DetailRow>
<DetailRow label="Uraian Operasi">-</DetailRow>
<DetailRow label="Jumlah Pendarahan">300 CC</DetailRow>
<DetailRow label="Specimen / Jaringan dikirim ke">PA</DetailRow>
</DE.Block>
<DE.Block>
<DetailRow label="Keterangan Jaringan">
<ul
class="list-disc space-y-1 pl-5 text-sm"
v-if="tissueNotes.length > 0"
v-for="item in tissueNotes"
>
<li>{{ item.note }}</li>
</ul>
<span v-else>-</span>
</DetailRow>
<DetailRow label="PRC">300 CC</DetailRow>
<DetailRow label="FPP">-</DetailRow>
<DetailRow label="WB">-</DetailRow>
<DetailRow label="TC">-</DetailRow>
<DetailRow label="Merk">-</DetailRow>
<DetailRow label="Nama Implant">-</DetailRow>
<DetailRow label="Sticker / Nomor Register Implant">-</DetailRow>
<DetailRow label="Nama Pendamping Implant">-</DetailRow>
</DE.Cell>
<DE.Cell>
<DetailRow label="Jenis Pembedahan">Bersih</DetailRow>
<DetailRow label="Operasi ke">1 (Satu)</DetailRow>
<DetailRow label="Keterangan Lahir">Lahir Hidup</DetailRow>
<DetailRow label="Ket. Tempat Lahir">RSSA</DetailRow>
<DetailRow label="Berat Badan">18 gram</DetailRow>
<DetailRow label="Ket. Saat Lahir">Normal dan sehat</DetailRow>
<DetailRow label="Uraian Operasi">-</DetailRow>
<DetailRow label="Jumlah Pendarahan">300 CC</DetailRow>
<DetailRow label="Specimen / Jaringan dikirim ke">PA</DetailRow>
<DetailRow label="Keterangan Jaringan">
<ul
class="list-disc space-y-1 pl-5 text-sm"
v-if="tissueNotes.length > 0"
v-for="item in tissueNotes"
>
<li>{{ item.note }}</li>
</ul>
<span v-else>-</span>
</DetailRow>
</DE.Cell>
</DE.Block>
</AccordionContent>
</AccordionItem>
@@ -1,63 +0,0 @@
<script setup lang="ts">
import mockData from './sample'
// type
import { genDoctor, type Doctor } from '~/models/doctor'
import type { TreatmentReportFormData } from '~/schemas/treatment-report.schema'
// components
import { toast } from '~/components/pub/ui/toast'
import AppTreatmentReportEntry from '~/components/app/treatment-report/entry-form.vue'
import ArrangementProcedurePicker from '~/components/app/therapy-protocol/picker-dialog/arrangement-procedure/procedure-picker.vue'
interface Props {
id: string | number
}
const props = defineProps<Props>()
const { backToList } = useQueryCRUDMode('mode')
const detail = ref<TreatmentReportFormData | null>({} as unknown as TreatmentReportFormData)
const doctors = ref<Doctor[]>([])
// TODO: dummy data
;(() => {
doctors.value = [genDoctor()]
})()
onMounted(() => {
// TODO: get data report by props.id
// mock
detail.value = mockData as unknown as TreatmentReportFormData
})
</script>
<template>
<AppTreatmentReportEntry
v-if="detail"
:isLoading="false"
:mode="'edit'"
@submit="(val) => console.log(val)"
@back="backToList"
@error="
(err: Error) => {
toast({
title: 'Terjadi Kesalahan',
description: err.message,
variant: 'destructive',
})
}
"
:doctors="doctors"
:initialValues="detail"
>
<template #procedures>
<ArrangementProcedurePicker
field-name="procedures"
title="Tindakan Operatif/Non-Operatif Lain"
sub-title="Pilih Prosedur"
/>
</template>
</AppTreatmentReportEntry>
</template>
@@ -1,6 +1,7 @@
<script setup lang="ts">
import List from './list.vue'
import Form from './form.vue'
import View from './view.vue'
// Models
import type { Encounter } from '~/models/encounter'
@@ -12,7 +13,7 @@ interface Props {
const props = defineProps<Props>()
const { mode, goToEntry, backToList } = useQueryCRUDMode('mode')
const { mode, goToEntry, goToView } = useQueryCRUDMode('mode')
</script>
<template>
@@ -21,11 +22,13 @@ const { mode, goToEntry, backToList } = useQueryCRUDMode('mode')
v-if="mode === 'list'"
:encounter="props.encounter"
@add="goToEntry"
@edit="goToEntry"
@edit="goToEntry({ fromView: false })"
@view="goToView"
/>
<Form
v-else
@back="backToList"
<View
v-else-if="mode === 'view'"
:encounter="props.encounter"
/>
<Form v-else />
</div>
</template>
@@ -12,7 +12,7 @@ import ArrangementProcedurePicker from '~/components/app/therapy-protocol/picker
// states
const route = useRoute()
const { mode, backToList } = useQueryCRUDMode('mode')
const { mode, goBack } = useQueryCRUDMode('mode')
const { recordId } = useQueryCRUDRecordId('record-id')
const reportData = ref<TreatmentReportFormData>({} as unknown as TreatmentReportFormData)
const doctors = ref<Doctor[]>([])
@@ -52,7 +52,7 @@ async function loadEntryForEdit(id: number | string) {
:isLoading="isLoading"
:mode="entryMode"
@submit="(val) => console.log(val)"
@back="backToList"
@back="goBack"
@error="
(err: Error) => {
toast({
@@ -23,6 +23,7 @@ const props = defineProps<Props>()
const emits = defineEmits<{
(e: 'add'): void
(e: 'edit', id: number | string): void
(e: 'view', id: number | string): void
}>()
// states
@@ -76,6 +77,17 @@ const goEdit = (id: number | string) => {
})
}
const goView = (id: number | string) => {
router.replace({
path: route.path,
query: {
...route.query,
mode: 'view',
'record-id': id,
},
})
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
@@ -90,22 +102,33 @@ async function onGetDetail(id: number | string) {
}
// #region watcher
watch([recId, recAction], () => {
switch (recAction.value) {
watch([recId, recAction], (newVal) => {
const [id, action] = newVal
// Guard: jangan proses jika id = 0 atau action kosong
if (!id || !action) return
switch (action) {
case ActionEvents.showDetail:
onGetDetail(recId.value)
// onGetDetail(recId.value)
goView(id)
title.value = 'Detail Konsultasi'
break
case ActionEvents.showEdit:
goEdit(recId.value)
// reset
recId.value = 0
goEdit(id)
title.value = 'Edit Konsultasi'
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
// Reset KEDUANYA menggunakan nextTick agar tidak trigger watcher lagi
nextTick(() => {
recId.value = 0
recAction.value = ''
})
})
// #endregion
</script>
@@ -3,15 +3,23 @@ import mockData from './sample'
// types
import { type TreatmentReportFormData } from '~/schemas/treatment-report.schema'
import { type Encounter } from '~/models/encounter'
// Components
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import AppTreatmentReportPreview from '~/components/app/treatment-report/preview.vue'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
// #region Props & Emits
const router = useRouter()
const { backToList, goToEntry } = useQueryCRUDMode('mode')
const { recordId } = useQueryCRUDRecordId('record-id')
function onEditFromView() {
goToEntry({ fromView: true })
}
const props = defineProps<{
id: string | number
encounter: Encounter
}>()
// #endregion
@@ -50,15 +58,10 @@ function onBack() {}
</script>
<template>
<Header
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
/>
<AppTreatmentReportPreview
v-if="reportData"
:data="reportData"
@back="onBack"
@edit="onEdit"
@back="backToList"
@edit="onEditFromView"
/>
</template>
+49 -5
View File
@@ -5,8 +5,15 @@ export function useQueryCRUDMode(key: string = 'mode') {
const route = useRoute()
const router = useRouter()
const mode = computed<'list' | 'entry'>({
get: () => (route.query[key] && route.query[key] === 'entry' ? 'entry' : 'list'),
const mode = computed<'list' | 'entry' | 'view'>({
get: () => {
const q = route.query[key]
if (q === 'entry') return 'entry'
if (q === 'view') return 'view'
return 'list'
},
set: (val) => {
router.push({
path: route.path,
@@ -18,20 +25,57 @@ export function useQueryCRUDMode(key: string = 'mode') {
},
})
const goToEntry = () => (mode.value = 'entry')
// Cek apakah form diakses dari view/detail
const fromView = computed(() => route.query['from'] === 'view')
const goToEntry = (options?: { fromView?: boolean }) => {
router.push({
path: route.path,
query: {
...route.query,
[key]: 'entry',
from: options?.fromView ? 'view' : undefined,
},
})
}
const goToView = () => (mode.value = 'view')
const backToList = () => {
router.push({
path: route.path,
query: {
...route.query,
mode: 'list',
// HAPUS record-id
// HAPUS record-id dan from
'record-id': undefined,
from: undefined,
},
})
}
return { mode, goToEntry, backToList }
const backToView = () => {
router.push({
path: route.path,
query: {
...route.query,
mode: 'view',
// HAPUS from
from: undefined,
},
})
}
// Fungsi back yang otomatis menentukan kemana harus kembali
const goBack = () => {
if (fromView.value) {
backToView()
} else {
backToList()
}
}
return { mode, fromView, goToEntry, goToView, backToList, backToView, goBack }
}
export function useQueryCRUDRecordId(key: string = 'record-id') {
@@ -1,46 +0,0 @@
<script setup lang="ts">
// import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
// import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
// middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Edit Laporan Tindakan',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
// const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
// const { checkRole, hasReadAccess } = useRBAC()
// // Check if user has access to this page
// const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
// const canRead = hasReadAccess(roleAccess)
const canRead = true
</script>
<template>
<div>
<ContentTreatmentReportEdit
v-if="canRead"
:id="route.params.id as string"
/>
<Error
v-else
:status-code="403"
/>
</div>
</template>
@@ -1,44 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Detail Laporan Tindakan',
contentFrame: 'cf-container-xl',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
const canRead = hasReadAccess(roleAccess)
</script>
<template>
<div>
<ContentTreatmentReportDetail
v-if="canRead"
:id="route.params.id as string"
/>
<Error
v-else
:status-code="403"
/>
</div>
</template>
@@ -1,45 +0,0 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah Laporan Tindakan',
contentFrame: 'cf-container-2xl',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
// const hasAccess = checkRole(roleAccess)
const hasAccess = true
if (!hasAccess) {
navigateTo('/403')
}
// Define permission-based computed properties
// const canRead = hasReadAccess(roleAccess)
const canRead = true
</script>
<template>
<div>
<div v-if="canRead">
<ContentTreatmentReportAdd />
</div>
<Error
v-else
:status-code="403"
/>
</div>
</template>