impl: form entry with source of add,edit

feat(treatment-report): add history list component and dialog integration

- Create new list-history.vue component for displaying treatment report history
- Add configuration file for history list data table
- Integrate history list into main treatment report view with dialog
- Replace simple filter button with action buttons including history view

feat(treatment-report): implement crud navigation and form actions

- Add new entry component for treatment report with list and form views
- Implement back navigation using useQueryCRUDMode
- Replace form submit button with action footer component
- Update dropdown action component for detail view

refactor(treatment-report): restructure form components and update mode handling

- Rename 'add.vue' to 'form.vue' for better clarity
- Update form mode types from 'create|update|view' to 'add|edit|view'
- Implement proper reactive state handling for form modes
- Add new form component with loading states and mock data
- Enhance list component with action handlers and navigation

fix default calculated hours
This commit is contained in:
Khafid Prayoga
2025-12-01 19:50:43 +07:00
parent 1b4d3af909
commit be1b86141f
10 changed files with 442 additions and 18 deletions
@@ -8,11 +8,13 @@ import { type TreatmentReportFormData, TreatmentReportSchema } from '~/schemas/t
// type
import type { Doctor } from '~/models/doctor'
import { type ClickType as ActionClickType } from '~/components/pub/my-ui/nav-footer/index'
// components
import * as DE from '~/components/pub/my-ui/doc-entry'
import Separator from '~/components/pub/ui/separator/Separator.vue'
import { ArrayMessage } from '~/components/pub/ui/form'
import ActionForm from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
// form field components
import {
@@ -39,7 +41,7 @@ interface FormData extends TreatmentReportFormData {
interface Props {
isLoading: boolean
mode?: 'create' | 'update' | 'view'
mode: 'add' | 'edit' | 'view'
initialValues?: Partial<FormData>
// form related
@@ -49,17 +51,20 @@ interface Props {
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'submit', payload: FormData): void
(e: 'back'): void
(e: 'error', errors: Error): void
}>()
const tissueNotesLimit = 5
const { isLoading, mode = 'create' } = props
const mode = toRef(props, 'mode')
const isLoading = toRef(props, 'isLoading')
const isReadonly = computed(() => {
if (isLoading) {
if (isLoading.value === true) {
return true
}
if (mode === 'view') {
if (mode.value === 'view') {
return true
}
@@ -93,7 +98,17 @@ defineExpose({
// #endregion region
// #region Utilities & event handlers
// const onSubmit = handleSubmit((formValues: FormData) => emit('submit', formValues))
const onFormActionClicked = (action: ActionClickType) => {
if (action === 'back') {
emit('back')
return
}
if (action === 'submit') {
onSubmit()
return
}
}
const onSubmit = handleSubmit(
(values) => {
console.log(JSON.stringify(values))
@@ -126,6 +141,7 @@ watch(
setFieldValue('_operationDuration', formatTime(res))
},
{ immediate: true }
)
watch(
@@ -147,6 +163,7 @@ watch(
setFieldValue('_anesthesiaDuration', formatTime(res))
},
{ immediate: true }
)
// #endregion
</script>
@@ -445,13 +462,7 @@ watch(
/>
</Fragment>
<div class="mt-4 flex justify-end">
<button
type="submit"
class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
:disabled="isLoading"
>
{{ isLoading ? 'Menyimpan...' : 'Simpan' }}
</button>
<ActionForm @click="onFormActionClicked" />
</div>
</form>
</template>
@@ -0,0 +1,88 @@
import { defineAsyncComponent } from 'vue'
import { format } from 'date-fns'
import { id } from 'date-fns/locale'
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import type { TreatmentReportData } from '~/components/app/treatment-report/sample'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-d.vue'))
export const config: Config = {
cols: [
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 50 },
],
headers: [
[
{ label: 'TANGGAL LAPORAN' },
{ label: 'DPJP' },
{ label: 'OPERATOR' },
{ label: 'TANGGAL PEMBEDAHAN' },
{ label: 'JENIS OPERASI' },
{ label: 'KODE BILLING' },
{ label: 'SISTEM OPERASI' },
{ label: 'AKSI' },
],
],
keys: ['reportAt', 'dpjp', 'operator', 'operationAt', 'operationType', 'billing', 'system', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
reportAt: (rec: unknown): unknown => {
const attr = (rec as TreatmentReportData).reportAt
const result = format(new Date(attr), 'd MMMM yyyy, HH:mm', { locale: id })
return result
},
operationAt: (rec: unknown): unknown => {
const attr = (rec as TreatmentReportData).operationAt
const result = format(new Date(attr), 'd MMMM yyyy', { locale: id })
return result
},
system: (rec: unknown): unknown => {
return 'Cito'
},
operator: (rec: unknown): unknown => {
return 'dr. Dewi Arum Sawitri, Sp.An'
},
billing: (rec: unknown): unknown => {
return 'General'
},
operationType: (rec: unknown): unknown => {
return 'Besar'
},
dpjp: (rec: unknown): unknown => {
return 'dr. Irwansyah Kurniawan Sp.Bo'
},
parent: (rec: unknown): unknown => {
const recX = rec as any
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
@@ -0,0 +1,39 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list.cfg'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
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"
/>
<PaginationView
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
</template>
+1 -1
View File
@@ -22,7 +22,7 @@ import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
import Radiology from '~/components/content/radiology-order/main.vue'
import Consultation from '~/components/content/consultation/list.vue'
import Cprj from '~/components/content/cprj/entry.vue'
import TreatmentReport from '~/components/content/treatment-report/list.vue'
import TreatmentReport from '~/components/content/treatment-report/entry.vue'
import DocUploadList from '~/components/content/document-upload/list.vue'
import GeneralConsentList from '~/components/content/general-consent/entry.vue'
import ResumeList from '~/components/content/resume/list.vue'
@@ -15,8 +15,9 @@ interface Props {
}
const props = defineProps<Props>()
const detail = ref<TreatmentReportFormData | null>(null)
const { backToList } = useQueryCRUDMode('mode')
const detail = ref<TreatmentReportFormData | null>({} as unknown as TreatmentReportFormData)
const doctors = ref<Doctor[]>([])
// TODO: dummy data
@@ -36,7 +37,9 @@ onMounted(() => {
<AppTreatmentReportEntry
v-if="detail"
:isLoading="false"
:mode="'edit'"
@submit="(val) => console.log(val)"
@back="backToList"
@error="
(err: Error) => {
toast({
@@ -47,7 +50,7 @@ onMounted(() => {
}
"
:doctors="doctors"
:initialValues="detail as unknown as TreatmentReportFormData"
:initialValues="detail"
>
<template #procedures>
<ArrangementProcedurePicker
@@ -0,0 +1,31 @@
<script setup lang="ts">
import List from './list.vue'
import Form from './form.vue'
// Models
import type { Encounter } from '~/models/encounter'
// Props
interface Props {
encounter: Encounter
}
const props = defineProps<Props>()
const { mode, goToEntry, backToList } = useQueryCRUDMode('mode')
</script>
<template>
<div>
<List
v-if="mode === 'list'"
:encounter="props.encounter"
@add="goToEntry"
@edit="goToEntry"
/>
<Form
v-else
@back="backToList"
/>
</div>
</template>
@@ -1,4 +1,6 @@
<script setup lang="ts">
import mockData from './sample'
// type
import { genDoctor, type Doctor } from '~/models/doctor'
import type { TreatmentReportFormData } from '~/schemas/treatment-report.schema'
@@ -8,18 +10,49 @@ 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'
// states
const route = useRoute()
const { mode, backToList } = useQueryCRUDMode('mode')
const { recordId } = useQueryCRUDRecordId('record-id')
const reportData = ref<TreatmentReportFormData>({} as unknown as TreatmentReportFormData)
const doctors = ref<Doctor[]>([])
const isLoading = ref<boolean>(false)
// TODO: dummy data
;(() => {
doctors.value = [genDoctor()]
})()
const entryMode = ref<'add' | 'edit' | 'view'>('add')
const isDataReady = ref(false)
onMounted(async () => {
if (mode.value === 'entry' && recordId.value) {
entryMode.value = 'edit'
await loadEntryForEdit(+recordId.value)
} else {
// Untuk mode 'add', langsung set ready
isDataReady.value = true
}
})
// TODO: map data
async function loadEntryForEdit(id: number | string) {
isLoading.value = true
const result = mockData
reportData.value = result as TreatmentReportFormData
isLoading.value = false
isDataReady.value = true
}
</script>
<template>
<AppTreatmentReportEntry
v-if="isDataReady"
:isLoading="isLoading"
:mode="entryMode"
@submit="(val) => console.log(val)"
@back="backToList"
@error="
(err: Error) => {
toast({
@@ -30,6 +63,7 @@ const isLoading = ref<boolean>(false)
}
"
:doctors="doctors"
:initialValues="reportData"
>
<template #procedures>
<ArrangementProcedurePicker
@@ -39,4 +73,10 @@ const isLoading = ref<boolean>(false)
/>
</template>
</AppTreatmentReportEntry>
<div
v-else
class="flex items-center justify-center p-8"
>
<p class="text-muted-foreground">Memuat data...</p>
</div>
</template>
@@ -1,15 +1,58 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import AppTreatmentReportList from '~/components/app/treatment-report/list.vue'
import AppTreatmentReportListHistory from '~/components/app/treatment-report/list-history.vue'
import { ButtonAction } from '~/components/pub/my-ui/form'
// types
import { ActionEvents } from '~/components/pub/my-ui/data/types'
// Samples
import { sampleRows, type TreatmentReportData } from '~/components/app/treatment-report/sample'
import sampleReport from './sample'
// Models
import type { Encounter } from '~/models/encounter'
// Props
interface Props {
encounter: Encounter
}
const props = defineProps<Props>()
const emits = defineEmits<{
(e: 'add'): void
(e: 'edit', id: number | string): void
}>()
// states
const router = useRouter()
const route = useRoute()
const { goToEntry, backToList } = useQueryCRUDMode('mode')
const title = ref('')
const search = ref('')
const dateFrom = ref('')
const dateTo = ref('')
const isDialogOpen = ref<boolean>(false)
const isLoading = ref<boolean>(false)
// #region mock
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/consultation.handler'
// #endregion
// filter + pencarian sederhana (client-side)
const filtered = computed(() => {
@@ -21,6 +64,50 @@ const filtered = computed(() => {
return true
})
})
const goEdit = (id: number | string) => {
router.replace({
path: route.path,
query: {
...route.query,
mode: 'entry',
'record-id': id,
},
})
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
async function onGetDetail(id: number | string) {
isLoading.value = true
const res = sampleReport
recItem.value = res
console.log(res)
isLoading.value = false
}
// #region watcher
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
onGetDetail(recId.value)
title.value = 'Detail Konsultasi'
break
case ActionEvents.showEdit:
goEdit(recId.value)
// reset
recId.value = 0
title.value = 'Edit Konsultasi'
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
// #endregion
</script>
<template>
@@ -51,9 +138,43 @@ const filtered = computed(() => {
type="date"
class="rounded border px-3 py-2"
/>
<ButtonAction
preset="custom"
title="Filter List Laporan Tindakan"
label="Filter"
icon="i-lucide-filter"
@click="
() => {
isDialogOpen = true
}
"
/>
</div>
<button class="ml-auto rounded bg-orange-500 px-3 py-2 text-white hover:bg-orange-600">Filter</button>
<div class="ml-auto flex items-center gap-2">
<ButtonAction
preset="custom"
title="Riwayat Laporan Tindakan"
icon="i-lucide-history"
label="Riwayat Laporan Tindakan"
@click="
() => {
isDialogOpen = true
}
"
/>
<ButtonAction
preset="add"
title="Tambah Data Laporan Tindakan"
icon="i-lucide-plus"
label="Tambah Data"
@click="
() => {
goToEntry()
}
"
/>
</div>
</div>
<div class="overflow-x-auto p-4">
@@ -70,4 +191,28 @@ const filtered = computed(() => {
/>
</div>
</div>
<Dialog
v-model:open="isDialogOpen"
title="Arsip Riwayat Laporan Tindakan"
size="2xl"
prevent-outside
@update:open="
(value: any) => {
isDialogOpen = value
}
"
>
<AppTreatmentReportListHistory
:data="filtered"
:pagination-meta="{
recordCount: 2,
page: 1,
pageSize: 10,
totalPage: 1,
hasPrev: false,
hasNext: false,
}"
/>
</Dialog>
</template>
@@ -0,0 +1,66 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from './types'
import { ActionEvents } 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')!
const timestamp = inject<Ref<number>>('timestamp')!
const activeKey = ref<string | null>(null)
const linkItems: LinkItem[] = [
{
label: 'Detail',
onClick: () => {
detail()
},
icon: 'i-lucide-eye',
},
]
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
timestamp.value = Date.now()
}
</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 linkItems"
: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>
@@ -0,0 +1 @@
export type ClickType = 'back' | 'draft' | 'submit'