Files
onboard/resources/js/Pages/Report/Components/ReportSectionItem.vue
2026-04-21 10:08:14 +09:00

549 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import {
NIcon,
NText,
NDataTable,
NButton,
NBadge,
NTabs,
NTabPane,
NPopover,
NEllipsis,
NTooltip,
NFlex,
NInput,
NPagination,
NSpin,
} from "naive-ui";
import {useReportStore} from "../../../Stores/report.js";
import {computed, h, ref, watch} from "vue";
import {storeToRefs} from "pinia";
import {TbEye, TbExternalLink, TbPencil} from "vue-icons-plus/tb";
import MoveModalComment from "./MoveModalComment.vue";
import OperationInfoModal from "./OperationInfoModal.vue";
import ManualPatientOutcomeModal from "./ManualPatientOutcomeModal.vue";
import ManualPatientLinkModal from "./ManualPatientLinkModal.vue";
import ManualPatientEditModal from "./ManualPatientEditModal.vue";
import ManualPatientOperationsModal from "./ManualPatientOperationsModal.vue";
import {useDebounceFn} from "@vueuse/core";
const props = defineProps({
id: {
type: String,
default: null
},
mode: {
type: String,
default: 'fillable' // 'fillable', 'readonly'
},
keys: {
type: Array,
default: ['num', 'fullname', 'age', 'birth_date', 'admitted_at']
},
status: {
type: String,
default: null // 'plan'
},
isRemovable: {
type: Boolean,
default: false
},
isDraggable: {
type: Boolean,
default: false
},
isDraggableDrop: {
type: Boolean,
default: false
},
accentIds: {
type: Array,
default: []
},
enabled: {
type: Boolean,
default: true,
},
})
const isFillableMode = computed(() => props.mode.toLowerCase() === 'fillable')
const isReadonlyMode = computed(() => props.mode.toLowerCase() === 'readonly')
const baseStatus = computed(() => props.status.replace(/^(mis|special)-/, ''))
const isSpecialStatus = computed(() => props.status.startsWith('special-'))
const tableRef = ref()
const emit = defineEmits(['item-dragged', 'item-dropped'])
const reportStore = useReportStore()
const {patientsData, statusStates} = storeToRefs(reportStore)
const showMoveModal = ref(false)
const showOperationInfoModal = ref(false)
const showManualOutcomeModal = ref(false)
const showManualLinkModal = ref(false)
const showManualEditModal = ref(false)
const showManualOperationsModal = ref(false)
const currentHistory = ref(null)
const latestDropItem = ref(null)
const activePatient = ref(null)
const hasDisabledEdit = computed(() => {
return !Boolean(reportStore.reportInfo?.report?.isActiveSendButton)
})
const canEditSpecial = computed(() => isSpecialStatus.value && !hasDisabledEdit.value)
const statusState = computed(() => statusStates.value[props.status] ?? {
page: 1,
perPage: 20,
total: 0,
search: '',
loading: false,
loaded: false,
})
const searchValue = ref('')
const isObservationStatus = computed(() => baseStatus.value === 'observation')
const showPagination = computed(() => !isObservationStatus.value)
// Добавляем drag колонку если режим fillable
const columns = computed(() => {
const resolvedBaseColumns = reportStore.getColumnsByKey(props.keys)
.filter(Boolean)
.map((column) => ({ ...column }))
const newColumns = []
const dragColumn = {
title: '',
key: 'goToMis',
width: 40,
render: (row) => {
const actions = []
if (canEditSpecial.value && row?.department_patient_id) {
actions.push(
h(
NTooltip,
{},
{
trigger: () => h(
'div',
{
style: {
cursor: 'pointer',
textAlign: 'center',
userSelect: 'none',
color: '#6b7280',
display: 'inline-flex',
},
onClick: () => {
activePatient.value = row
showManualEditModal.value = true
},
},
[h(NIcon, { depth: 3, size: 16 }, h(TbPencil))]
),
default: () => 'Редактировать',
}
)
)
}
if (row.medical_history_id) {
actions.push(
h(
NTooltip,
{},
{
trigger: () => h(
'div',
{
style: {
cursor: 'pointer',
textAlign: 'center',
userSelect: 'none',
display: 'inline-flex',
},
onClick: () => {
window.open(`https://stationar.amurzdrav.ru/prod/statist/edit/card/${row.medical_history_id}`, '_blank')
}
},
[h(NIcon, { depth: 2 }, h(TbExternalLink))]
),
default: () => 'Перейти в карту'
}
)
)
}
if (!actions.length) {
return null
}
if (actions.length === 1) {
return actions[0]
}
return h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
},
},
actions
)
}
}
const removeColumn = {
title: '',
key: 'remove',
render: (row) => h(
NButton,
{
text: true,
disabled: hasDisabledEdit.value,
onClick: () => {
axios.post('/api/report/observation/remove', {
id: row.id
}).then(async () => {
reportStore.removeObservationPatientLocally(row.id)
})
}
},
[
'Снять с наблюдения'
]
)
}
const expandColumn = {
title: '',
width: '30',
render: (rowData) => {
return h(
NIcon,
{
onClick: () => {
latestDropItem.value = rowData
showMoveModal.value = true
}
},
{
default: h(TbEye)
}
)
}
}
const fillableColumn = {
title: '',
key: 'fillable',
width: '20',
render: (row) => h(
NBadge,
{
dot: true,
color: (row.comment && row.comment.trim()) ? '#7fe7c4' : '#e88080'
}
)
}
if (baseStatus.value === 'observation') {
newColumns.push(expandColumn)
newColumns.push(fillableColumn)
}
newColumns.push(dragColumn)
newColumns.push(...resolvedBaseColumns)
newColumns.push({
title: 'Диагноз',
key: 'ds',
render: (row) => {
if (row.mkb.ds !== null && row.mkb.name !== null) {
return h(NPopover, {
placement: 'top',
arrow: false
}, {
trigger: h(
'div',
{
class: 'cursor-help w-full',
style: 'padding: 8px;'
},
row.mkb.ds
),
default: row.mkb.name
})
} else {
return row.mkb.ds
}
},
cellProps: (rowData, rowIndex) => {
const styles = ['--n-td-padding: 0;']
return {
'style': styles
}
}
})
if (props.isRemovable) newColumns.push(removeColumn)
if (baseStatus.value === 'emergency' || baseStatus.value === 'plan') {
const operationColumn = {
title: 'Операции',
key: 'operations',
render: (row) => canEditSpecial.value && row?.department_patient_id
? h(
'div',
{
class: 'underline decoration-dashed cursor-pointer',
style: 'padding: 8px;',
onClick: () => {
activePatient.value = row
showManualOperationsModal.value = true
},
},
row.operations?.length
? h(NEllipsis, {tooltip: false}, row.operations.map(itm => `${itm.code}; `).join(''))
: 'Добавить'
)
: isSpecialStatus.value
? (row.operations?.length
? h('div', {style: 'padding: 8px;'}, h(NEllipsis, {tooltip: false}, row.operations.map(itm => `${itm.code}; `).join('')))
: h('div', {style: 'padding: 8px;'}, '-'))
: row.operations?.length ?
h(
NPopover,
{
placement: 'top',
arrow: false
},
{
trigger: () => h(
'div',
{
class: 'underline decoration-dashed cursor-pointer',
style: 'padding: 8px;',
onClick: () => {
currentHistory.value = row.medical_history_id
showOperationInfoModal.value = true
},
},
h(NEllipsis, {tooltip: false}, row.operations.map(itm => `${itm.code}; `).join(''))
),
default: () => row.operations.map(itm =>`${itm.name}; `).join(''),
}
) : h('div', {style: 'padding: 8px;',}, '-'),
cellProps: (rowData, rowIndex) => {
const styles = ['--n-td-padding: 0;']
return {
'style': styles
}
}
}
newColumns.push(operationColumn)
}
if (baseStatus.value === 'outcome') {
const typeColumn = {
title: 'Причина',
key: 'outcome_type',
ellipsis: {
tooltip: true
}
}
newColumns.push(typeColumn)
}
return newColumns
})
const isCursorOverTable = (e) => {
const rect = tableRef.value.$el.getBoundingClientRect()
return (
e.clientX >= rect.left &&
e.clientX <= rect.right &&
e.clientY >= rect.top &&
e.clientY <= rect.bottom
)
}
const handleDragStart = (e, row) => {
if (hasDisabledEdit.value) return
// Устанавливаем данные о перетаскиваемом элементе
e.dataTransfer.setData('application/json', JSON.stringify({
row: row,
fromStatus: props.status
}))
e.dataTransfer.effectAllowed = 'copy'
const rowElement = e.target
// Устанавливаем флаг перетаскивания в store
reportStore.isDragActive = true
// Эмитим событие для родителя
emit('item-dragged', { row, fromStatus: props.status })
// Добавляем класс для визуальной обратной связи
e.target.classList.add('dragging')
}
const handleDragEnd = (e) => {
if (hasDisabledEdit.value) return
// Устанавливаем флаг перетаскивания в store
reportStore.isDragActive = false
if (e.target) {
e.target.classList.remove('dragging')
}
}
const handleDragOver = (e) => {
if (hasDisabledEdit.value) return
// console.log(e)
e.preventDefault()
}
const handleDrop = (e) => {
e.preventDefault()
if (hasDisabledEdit.value) return
if (props.isDraggableDrop === false) return
// Проверяем, действительно ли курсор над таблицей
if (!isCursorOverTable(e)) {
return
}
try {
const dragData = JSON.parse(e.dataTransfer.getData('application/json'))
// Эмитим событие для родителя
emit('item-dropped', {
item: dragData.row,
fromStatus: dragData.fromStatus,
toStatus: props.status
})
latestDropItem.value = dragData.row
showMoveModal.value = true
} catch (error) {
console.error('Drop error:', error)
}
}
function rowProps(row) {
const style = []
const classes = []
style.push(props.isDraggable ? 'cursor: grab;' : 'cursor: arrow;')
if (props.accentIds.length) {
if (props.accentIds.includes(row.id)) {
style.push('--n-merged-td-color: #047857')
}
}
return {
draggable: props.isDraggable,
style: style,
onDragstart: (e) => {
if (!props.isDraggable) return
handleDragStart(e, row)
},
onDragend: (e) => {
if (!props.isDraggableDrop) return
handleDragEnd(e)
},
onDragover: (e) => {
if (!props.isDraggableDrop) return
handleDragOver(e)
},
onDrop: (e) => {
if (!props.isDraggableDrop) return
handleDrop(e)
}
}
}
const applySearch = useDebounceFn(async () => {
await reportStore.setStatusSearch(props.status, searchValue.value)
}, 350)
const handlePageChange = async (page) => {
await reportStore.setStatusPage(props.status, page)
}
watch(() => props.enabled, async (enabled) => {
if (enabled) {
await reportStore.ensureStatusLoaded(props.status)
}
}, { immediate: true })
watch(() => reportStore.reportInfo?.dates, async () => {
searchValue.value = ''
if (props.enabled) {
await reportStore.loadPatientsByStatus(props.status, { resetPage: true })
}
}, { deep: true })
</script>
<template>
<div>
<NFlex vertical :size="8">
<NInput
v-model:value="searchValue"
clearable
size="small"
placeholder="Поиск по ФИО или диагнозу"
@update:value="applySearch"
/>
<NSpin :show="statusState.loading">
<NDataTable :columns="columns"
ref="tableRef"
:data="patientsData[status]"
size="small"
@drop="handleDrop"
@dragover="handleDragOver"
max-height="200"
min-height="200"
:row-props="rowProps"
:row-key="(row, index) => row.id"
class="text-sm!">
</NDataTable>
</NSpin>
<NPagination
v-if="showPagination"
:page="statusState.page"
:page-size="statusState.perPage"
:item-count="statusState.total"
:page-slot="7"
@update:page="handlePageChange"
/>
</NFlex>
<MoveModalComment v-model:show="showMoveModal" :patient-id="latestDropItem?.id" />
<OperationInfoModal v-model:show="showOperationInfoModal" :history-id="currentHistory" />
<ManualPatientOutcomeModal v-model:show="showManualOutcomeModal" :patient="activePatient" />
<ManualPatientLinkModal v-model:show="showManualLinkModal" :patient="activePatient" />
<ManualPatientEditModal v-model:show="showManualEditModal" :patient="activePatient" :source-status="status" />
<ManualPatientOperationsModal v-model:show="showManualOperationsModal" :patient="activePatient" :source-status="status" />
</div>
</template>
<style scoped>
:deep(.n-data-table-th),
:deep(.n-data-table-td) {
white-space: nowrap !important;
font-size: var(--n-font-size);
}
</style>