done: edit, detail report

fix: parsing err datetime on edit mode

todo: koordinasi dengan tim be terkait datetime iso

impl edit form

feat(treatment-report): add detail view and preview components

- Implement treatment report detail page with RBAC checks
- Create preview component with accordion layout
- Extract mock data to shared sample file
- Enhance procedure picker with preview mode
- Update schema to make procedure id required
- Improve detail row styling and layout

feat(treatment-report): update page titles and enhance preview component

- Update page titles for treatment report pages to be more descriptive
- Implement date formatting in treatment report preview
- Add router navigation for edit functionality
- Enhance preview component with detailed operation data sections
- Add support for tissue notes display in preview
This commit is contained in:
Khafid Prayoga
2025-11-28 13:21:35 +07:00
parent ccefb69f0c
commit 1b4d3af909
11 changed files with 559 additions and 119 deletions
@@ -1,10 +1,8 @@
<script setup lang="ts">
import { cn } from '~/lib/utils'
// types
import { type ProcedureSrc } from '~/models/procedure-src'
// componenets
// Components
import { FieldArray } from 'vee-validate'
import { ButtonAction } from '~/components/pub/my-ui/form'
import TableHeader from '~/components/pub/ui/table/TableHeader.vue'
@@ -14,69 +12,129 @@ interface Props {
fieldName: string
title: string
subTitle?: string
isReadonly?: boolean
}
const props = defineProps<Props>()
const { isReadonly = false } = props
// State UI (Loading / Disabled)
isReadonly?: boolean
// Data Architecture Switch
// 'form' = Pakai Vee-Validate (Parent wajib useForm)
// 'preview' = Pakai Props sampleItems (Parent bebas)
mode?: 'form' | 'preview'
// Data Source untuk mode 'preview' (atau initial data)
sampleItems?: ProcedureSrc[]
}
// Set default mode ke 'form' agar backward compatible
const props = withDefaults(defineProps<Props>(), {
mode: 'form',
isReadonly: false,
sampleItems: () => [],
})
const isProcedurePickerDialogOpen = ref<boolean>(false)
provide(`isProcedurePickerDialogOpen`, isProcedurePickerDialogOpen)
</script>
<template>
<div class="">
<div class="mb-2 flex items-center justify-between">
<p class="mb-2 font-medium">{{ title }}</p>
<ButtonAction
preset="add"
title="Tambah Item"
icon="i-lucide-search"
:disabled="isReadonly"
:label="subTitle || 'Pilih Diagnosis'"
:full-width-mobile="true"
@click="isProcedurePickerDialogOpen = true"
/>
</div>
<div class="mb-2 flex items-center justify-between">
<p class="mb-2 font-medium">{{ title }}</p>
<FieldArray
v-slot="{ fields, push, remove }"
:name="props.fieldName"
>
<ProcedureListDialog :process-fn="push" />
<ButtonAction
v-if="mode === 'form' && !isReadonly"
preset="add"
title="Tambah Item"
icon="i-lucide-search"
:label="subTitle || 'Pilih Diagnosis'"
:full-width-mobile="true"
@click="isProcedurePickerDialogOpen = true"
/>
</div>
<div class="overflow-hidden rounded-lg border border-gray-200">
<Table>
<TableHeader class="bg-gray-100">
<TableRow>
<TableHead class="w-1/2">Prosedur</TableHead>
<TableHead class="w-1/2">ICD-X</TableHead>
<TableHead class="w-1/2">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="(field, idx) in fields"
:key="idx"
<FieldArray
v-if="mode === 'form'"
v-slot="{ fields, push, remove }"
:name="props.fieldName"
>
<ProcedureListDialog :process-fn="push" />
<div class="overflow-hidden rounded-lg border border-gray-200">
<Table>
<TableHeader class="bg-gray-100">
<TableRow>
<TableHead class="w-1/2">Prosedur</TableHead>
<TableHead class="w-1/2">ICD-X</TableHead>
<TableHead class="w-[50px]">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="fields.length === 0">
<TableCell
colspan="3"
class="py-4 text-center text-muted-foreground"
>
<TableCell :class="cn(isReadonly && 'text-muted-foreground')">
{{ (field.value as ProcedureSrc)?.name }}
</TableCell>
<TableCell :class="cn(isReadonly && 'text-muted-foreground')">
{{ (field.value as ProcedureSrc)?.code }}
</TableCell>
<TableCell class="">
<ButtonAction
:disabled="isReadonly"
preset="delete"
:title="`Hapus prosedur '${(field.value as ProcedureSrc)?.name}'`"
icon-only
@click="remove(idx)"
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</FieldArray>
Belum ada data dipilih.
</TableCell>
</TableRow>
<TableRow
v-for="(field, idx) in fields"
:key="field.key"
>
<TableCell :class="cn(isReadonly && 'opacity-50')">
{{ (field.value as ProcedureSrc)?.name }}
</TableCell>
<TableCell :class="cn(isReadonly && 'opacity-50')">
{{ (field.value as ProcedureSrc)?.code }}
</TableCell>
<TableCell>
<ButtonAction
v-if="!isReadonly"
preset="delete"
icon-only
:title="`Hapus ${(field.value as ProcedureSrc)?.name}`"
@click="remove(idx)"
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</FieldArray>
<div
v-else
class="overflow-hidden rounded-lg border border-gray-200"
>
<Table>
<TableHeader class="bg-gray-100">
<TableRow>
<TableHead class="w-1/2">Prosedur</TableHead>
<TableHead class="w-1/2">ICD-X</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="sampleItems.length === 0">
<TableCell
colspan="2"
class="py-4 text-center text-muted-foreground"
>
Tidak ada data.
</TableCell>
</TableRow>
<TableRow
v-for="item in sampleItems"
:key="item.code || item.id"
>
<TableCell class="text-muted-foreground">
{{ item.name }}
</TableCell>
<TableCell class="text-muted-foreground">
{{ item.code }}
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</template>
@@ -0,0 +1,151 @@
<script setup lang="ts">
import { format } from 'date-fns'
import { id } from 'date-fns/locale'
// type
import { type ProcedureSrc } from '~/models/procedure-src'
import { type TreatmentReportFormData } from '~/schemas/treatment-report.schema'
// componenets
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '~/components/pub/ui/accordion'
import * as DE from '~/components/pub/my-ui/doc-entry'
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
import ArrangementProcedurePicker from '~/components/app/therapy-protocol/picker-dialog/arrangement-procedure/procedure-picker.vue'
// #region Props & Emits
const props = defineProps<{
data: TreatmentReportFormData
}>()
const emit = defineEmits<{
(e: 'back'): void
(e: 'edit'): void
}>()
// #endregion
// #region State & Computed
const { operatorTeam, procedures, operationExecution, bloodInput, implant, specimen, tissueNotes = [] } = props.data
const procedureSampleData = procedures as unknown as ProcedureSrc[]
// #region Lifecycle Hooks
// #endregion
// #region Functions
// #endregion region
// #region Utilities & event handlers
function onNavigate(type: string) {
if (type == 'back') emit('back')
if (type == 'edit') emit('edit')
}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<DetailRow label="Tanggal Laporan">
{{ format(new Date(), 'd MMMM yyyy, HH:mm', { locale: id }) }}
</DetailRow>
<Accordion
type="multiple"
class="w-full"
collapsible
:default-value="['section-1', 'section-2', 'section-3']"
>
<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>
</AccordionContent>
</AccordionItem>
<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"
/>
</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>
<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>
</DE.Block>
</AccordionContent>
</AccordionItem>
</Accordion>
<div class="my-2 flex justify-end py-2">
<PubMyUiNavFooterBaEd @click="onNavigate" />
</div>
</template>
<style scoped></style>
@@ -9,7 +9,7 @@ import AppTreatmentReportEntry from '~/components/app/treatment-report/entry-for
import ArrangementProcedurePicker from '~/components/app/therapy-protocol/picker-dialog/arrangement-procedure/procedure-picker.vue'
const doctors = ref<Doctor[]>([])
const isLoading = ref<boolean>(false)
// TODO: dummy data
;(() => {
doctors.value = [genDoctor()]
@@ -18,7 +18,7 @@ const doctors = ref<Doctor[]>([])
<template>
<AppTreatmentReportEntry
:isLoading="false"
:isLoading="isLoading"
@submit="(val) => console.log(val)"
@error="
(err: Error) => {
@@ -30,58 +30,6 @@ const doctors = ref<Doctor[]>([])
}
"
:doctors="doctors"
:initialValues="
{
operatorTeam: {
dpjpId: -1,
operatorName: 'Julian',
assistantOperatorName: 'Amar',
instrumentNurseName: 'Anang',
surgeryDate: '2025-11-13T14:29',
actionDiagnosis: 'Omon Omon Saja',
},
procedures: [
{
id: -1,
name: 'Ndase mumet',
code: 'CX1',
},
],
operationExecution: {
surgeryType: 'khusus',
billingCode: 'local',
operationSystem: 'cito',
surgeryCleanType: 'kotor',
surgeryNumber: 'retry',
birthPlaceNote: 'out3',
personWeight: 100,
operationDescription: 'asdsadsa1',
birthRemark: 'lahir_hidup',
},
bloodInput: {
type: 'tc',
amount: {
prc: null,
wb: null,
ffp: null,
tc: 3243324,
},
},
implant: {
brand: 'Samsung',
name: 'S.komedi',
companionName: 'When ya',
},
specimen: {
destination: 'pa',
},
tissueNotes: [
{
note: 'Anjai',
},
],
} as unknown as TreatmentReportFormData
"
>
<template #procedures>
<ArrangementProcedurePicker
@@ -0,0 +1,64 @@
<script setup lang="ts">
import mockData from './sample'
// types
import { type TreatmentReportFormData } from '~/schemas/treatment-report.schema'
// Components
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
// #region Props & Emits
const router = useRouter()
const props = defineProps<{
id: string | number
}>()
// #endregion
// #region State & Computed
const reportData = ref<TreatmentReportFormData | null>(null)
const headerPrep: HeaderPrep = {
title: 'Detail Laporan Tindakan',
icon: 'i-lucide-stethoscope',
}
// #endregion
// #region Lifecycle Hooks
onMounted(async () => {
reportData.value = mockData as unknown as TreatmentReportFormData
})
// #endregion
// #region Functions
// #endregion region
// #region Utilities & event handlers
function onEdit() {
router.push({
name: 'treatment-report-id-edit',
params: { id: 100 },
})
}
function onBack() {}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<Header
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
/>
<AppTreatmentReportPreview
v-if="reportData"
:data="reportData"
@back="onBack"
@edit="onEdit"
/>
</template>
@@ -0,0 +1,60 @@
<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 detail = ref<TreatmentReportFormData | null>(null)
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"
@submit="(val) => console.log(val)"
@error="
(err: Error) => {
toast({
title: 'Terjadi Kesalahan',
description: err.message,
variant: 'destructive',
})
}
"
:doctors="doctors"
:initialValues="detail as unknown as TreatmentReportFormData"
>
<template #procedures>
<ArrangementProcedurePicker
field-name="procedures"
title="Tindakan Operatif/Non-Operatif Lain"
sub-title="Pilih Prosedur"
/>
</template>
</AppTreatmentReportEntry>
</template>
@@ -0,0 +1,68 @@
export default {
operatorTeam: {
dpjpId: -1,
operatorName: 'Julian Alvarez',
assistantOperatorName: 'Arda Guller',
instrumentNurseName: 'Kenan Yildiz',
surgeryDate: '2025-11-13T14:29:00',
actionDiagnosis: 'Sprei gratisnya mana',
},
procedures: [
{
id: -1,
name: 'Ndase mumet',
code: 'CX1',
},
],
operationExecution: {
surgeryType: 'khusus',
billingCode: 'local',
operationSystem: 'cito',
surgeryCleanType: 'kotor',
surgeryNumber: 'retry',
birthPlaceNote: 'out3',
personWeight: 100,
operationDescription: 'asdsadsa1',
birthRemark: 'lahir_hidup',
operationStartAt: '2025-11-13T14:29:00',
operationEndAt: '2025-11-13T17:29:00',
anesthesiaStartAt: '2025-11-13T11:29:00',
anesthesiaEndAt: '2025-11-13T18:29:00',
},
bloodInput: {
type: 'tc',
amount: {
prc: null,
wb: null,
ffp: null,
tc: 3243324,
},
},
implant: {
brand: 'Samsung',
name: 'S.Komedi',
companionName: 'When ya',
},
specimen: {
destination: 'pa',
},
tissueNotes: [
{
note: 'Anjai',
},
{
note: 'Ciee Kaget',
},
{
note: 'Baper',
},
{
note: 'Saltink weeh',
},
{
note: 'Kaburrr',
},
],
}
@@ -5,15 +5,17 @@ defineProps<{
</script>
<template>
<div class="flex flex-col gap-1 lg:grid lg:grid-cols-[180px_minmax(0,1fr)] lg:gap-x-3">
<div class="grid grid-cols-[150px_10px_1fr] gap-y-1 lg:grid-cols-[180px_12px_1fr]">
<!-- Label -->
<span class="text-md font-normal text-muted-foreground">
<span class="text-md font-normal tracking-wide text-muted-foreground">
{{ label }}
</span>
<!-- Colon -->
<span class="text-md tracking-wide text-muted-foreground">:</span>
<!-- Value -->
<span class="truncate lg:whitespace-normal">
<span class="me-3 hidden lg:inline-block">:</span>
<span class="text-md font-sans tracking-wide">
<slot />
</span>
</div>
@@ -0,0 +1,46 @@
<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>
@@ -0,0 +1,44 @@
<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>
@@ -6,7 +6,7 @@ import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah Laporan',
title: 'Tambah Laporan Tindakan',
contentFrame: 'cf-container-2xl',
})
+1 -2
View File
@@ -7,7 +7,6 @@ const isoDateTime = z
const date = new Date(val)
return !isNaN(date.getTime())
}, 'Format tanggal / waktu tidak valid')
.transform((val) => new Date(val).toISOString())
const positiveInt = z.coerce.number().int().nonnegative()
@@ -34,7 +33,7 @@ const OperatorTeamSchema = z.object({
})
const ProcedureSchema = z.object({
id: z.number().int().optional(),
id: z.number().int(),
name: z.string().min(1),
code: z.string().min(1),
})