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:
@@ -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>
|
||||
@@ -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>
|
||||
+40
@@ -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'
|
||||
Reference in New Issue
Block a user