666 lines
21 KiB
Vue
666 lines
21 KiB
Vue
<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, TbTrash, TbLock, TbEdit} 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 ReanimationIndicatorModal from "./ReanimationIndicatorModal.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 showReanimationIndicatorModal = ref(false)
|
||
const currentHistory = ref(null)
|
||
const latestDropItem = ref(null)
|
||
const activePatient = ref(null)
|
||
|
||
const hasDisabledEdit = computed(() => {
|
||
return !Boolean(reportStore.reportInfo?.report?.isActiveSendButton)
|
||
})
|
||
const isHeadOrAdmin = computed(() => Boolean(reportStore.reportInfo?.report?.isHeadOrAdmin))
|
||
const canEditSpecial = computed(() => (
|
||
isSpecialStatus.value
|
||
&& (!hasDisabledEdit.value || isHeadOrAdmin.value)
|
||
&& baseStatus.value !== 'observation'
|
||
))
|
||
const canEditReanimationIndicator = computed(() => (
|
||
baseStatus.value === 'reanimation'
|
||
&& !isSpecialStatus.value
|
||
&& (!hasDisabledEdit.value || isHeadOrAdmin.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,
|
||
align: 'left',
|
||
titleAlign: 'left',
|
||
}))
|
||
|
||
const newColumns = []
|
||
|
||
const dragColumn = {
|
||
title: '',
|
||
key: 'goToMis',
|
||
width: 40,
|
||
align: 'left',
|
||
titleAlign: 'left',
|
||
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',
|
||
align: 'left',
|
||
titleAlign: 'left',
|
||
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)
|
||
})
|
||
}
|
||
},
|
||
[
|
||
h(NIcon, {size: '16'}, h(TbTrash))
|
||
]
|
||
)
|
||
}
|
||
|
||
const expandColumn = {
|
||
title: '',
|
||
width: '30',
|
||
align: 'left',
|
||
titleAlign: 'left',
|
||
render: (rowData) => {
|
||
return h(
|
||
NIcon,
|
||
{
|
||
onClick: () => {
|
||
latestDropItem.value = rowData
|
||
showMoveModal.value = true
|
||
}
|
||
},
|
||
{
|
||
default: h(TbEye)
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
const fillableColumn = {
|
||
title: '',
|
||
key: 'fillable',
|
||
width: '20',
|
||
align: 'left',
|
||
titleAlign: 'left',
|
||
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',
|
||
align: 'left',
|
||
titleAlign: 'left',
|
||
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',
|
||
align: 'left',
|
||
titleAlign: 'left',
|
||
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',
|
||
align: 'left',
|
||
titleAlign: 'left',
|
||
ellipsis: {
|
||
tooltip: true
|
||
}
|
||
}
|
||
newColumns.push(typeColumn)
|
||
}
|
||
|
||
if (baseStatus.value === 'reanimation') {
|
||
const indicatorColumn = {
|
||
title: 'Состояние',
|
||
key: 'reanimation_indicator',
|
||
width: 120,
|
||
minWidth: 100,
|
||
maxWidth: 140,
|
||
align: 'left',
|
||
titleAlign: 'left',
|
||
render: (row) => {
|
||
const labelMap = {
|
||
stable: 'Стабильный',
|
||
moderate: 'Средней тяжести',
|
||
severe: 'Тяжелый',
|
||
critical: 'Критический',
|
||
}
|
||
const value = row.reanimation_indicator ? (labelMap[row.reanimation_indicator] ?? row.reanimation_indicator) : '-'
|
||
|
||
if (canEditReanimationIndicator.value) {
|
||
return h(
|
||
'div',
|
||
{
|
||
class: 'inline-flex items-center gap-1',
|
||
},
|
||
[
|
||
h(
|
||
'div',
|
||
{
|
||
class: 'underline decoration-dashed cursor-pointer',
|
||
onClick: () => {
|
||
activePatient.value = row
|
||
showReanimationIndicatorModal.value = true
|
||
},
|
||
},
|
||
value
|
||
),
|
||
h(
|
||
NTooltip,
|
||
{},
|
||
{
|
||
trigger: () => h(
|
||
'div',
|
||
{
|
||
class: 'cursor-pointer inline-flex text-slate-500',
|
||
onClick: () => {
|
||
activePatient.value = row
|
||
showReanimationIndicatorModal.value = true
|
||
},
|
||
},
|
||
[h(NIcon, { size: 14 }, h(TbEdit))]
|
||
),
|
||
default: () => 'Изменить состояние',
|
||
}
|
||
),
|
||
]
|
||
)
|
||
}
|
||
|
||
return h(
|
||
NTooltip,
|
||
{},
|
||
{
|
||
trigger: () => h(
|
||
'div',
|
||
{
|
||
class: 'inline-flex items-center gap-1 text-slate-500',
|
||
},
|
||
[
|
||
h('span', value),
|
||
h(NIcon, { size: 14 }, h(TbLock)),
|
||
]
|
||
),
|
||
default: () => 'Редактирование недоступно',
|
||
}
|
||
)
|
||
},
|
||
}
|
||
newColumns.push(indicatorColumn)
|
||
}
|
||
|
||
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" />
|
||
<ReanimationIndicatorModal v-model:show="showReanimationIndicatorModal" :patient="activePatient" />
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
:deep(.n-data-table-th),
|
||
:deep(.n-data-table-td) {
|
||
font-size: var(--n-font-size);
|
||
text-align: left !important;
|
||
white-space: nowrap !important;
|
||
}
|
||
|
||
:deep(.n-data-table-th__title),
|
||
:deep(.n-data-table-td__content) {
|
||
justify-content: flex-start !important;
|
||
}
|
||
</style>
|