* восстановление окна наблюдения

* добавил получение выбывших
* фильтрация выбывших по результатам лечения
* добавил подсказку при наведении на операции
* добавил вывод причины наблюдения
* добавил вкладки для выбывших
* изменил связь и сохранение пациентов на контроле
* добавил возможность редактирования причины контроля
* полное изменение окна с нежелательными событиями
* исправил просмотр причины контроля
* работа над окном редактирования причины контроля в таблице
* визуальное выделение умерших и проведенных операций
* добавил выбор даты для роли врач
* центрирование блоков статистики
* разделение выполненных операций на срочность
* поправил метод определения текущего дня для роли врач
* функция блокировки при выборе другой даты для роли врач
This commit is contained in:
brusnitsyn
2026-01-29 16:42:42 +09:00
parent cb43c74a72
commit 87e21f0e08
24 changed files with 2065 additions and 501 deletions

View File

@@ -0,0 +1,56 @@
<script setup>
import {NModal, NForm, NFormItem, NInput, NButton} from 'naive-ui'
import {useReportStore} from "../../../Stores/report.js";
import {ref, watch} from "vue";
const show = defineModel('show')
const props = defineProps({
patientId: {
type: Number,
default: null
}
})
const reportStore = useReportStore()
const droppedPatient = ref(null)
const rules = {
comment: {
required: true,
trigger: ['blur'],
message: 'Введите описание причины'
}
}
watch(() => props.patientId, (newPatientId) => {
if (newPatientId) {
const patientIndex = reportStore.patientsData.observation.findIndex(itm => itm.id === newPatientId)
const patient = reportStore.patientsData.observation[patientIndex]
droppedPatient.value = patient
}
})
</script>
<template>
<NModal v-model:show="show"
title="Постановка на контроль"
:mask-closable="false"
:closable="false"
class="max-w-lg"
preset="card"
>
<NForm :model="droppedPatient" :rules="rules">
<NFormItem label="Опишите причину" path="comment">
<NInput type="textarea" :rows="6" :resizable="false" v-model:value="droppedPatient.comment" />
</NFormItem>
</NForm>
<template #action>
<div class="flex justify-end">
<NButton type="primary" secondary @click="show = false">
Сохранить
</NButton>
</div>
</template>
</NModal>
</template>
<style scoped>
</style>

View File

@@ -5,6 +5,7 @@ import ReportFormInput from "./ReportFormInput.vue";
import ReportSection from "./ReportSection.vue";
import {useReportStore} from "../../../Stores/report.js";
import {useAuthStore} from "../../../Stores/auth.js";
import {computed} from "vue";
const props = defineProps({
mode: {
@@ -21,17 +22,19 @@ const onSubmit = () => {
departmentId: authStore.userDepartment.department_id
})
}
</script>
<template>
<NFlex vertical class="max-w-6xl mx-auto mt-6 mb-4 w-full">
<ReportHeader :mode="mode" />
<ReportFormInput v-if="mode === 'fillable'" />
<ReportFormInput />
<ReportSection label="Планово" />
<NButton secondary size="large" @click="onSubmit">
<NButton v-if="reportStore.reportInfo?.report.isActiveSendButton" secondary size="large" @click="onSubmit">
Сохранить отчет
</NButton>
</NFlex>

View File

@@ -1,30 +1,69 @@
<script setup>
import {NCard, NSkeleton, NFlex, NFormItem, NForm, NInputNumber} from "naive-ui";
import {NCard, NSkeleton, NSpace, NFlex, NFormItem, NForm, NInputNumber, NStatistic} from "naive-ui";
import {useReportStore} from "../../../Stores/report.js";
import {useAuthStore} from "../../../Stores/auth.js";
const reportStore = useReportStore()
const authStore = useAuthStore()
</script>
<template>
<NCard>
<NForm>
<NFlex>
<div class="grid grid-cols-[1fr_auto] gap-x-3">
<NCard v-if="reportStore.reportInfo?.report.isActiveSendButton">
<NForm>
<template v-if="reportStore.isLoadReportInfo">
<NSkeleton class="w-[246px]! h-[60px]!" />
<NSkeleton class="w-[246px]! h-[60px]!" />
<NSkeleton class="w-[246px]! h-[60px]!" />
<NFlex>
<NSkeleton class="rounded-md w-[246px]! h-[65px]!" />
<NSkeleton class="rounded-md w-[246px]! h-[65px]!" />
<NSkeleton class="rounded-md w-[246px]! h-[65px]!" />
</NFlex>
</template>
<template v-for="metrikaItem in reportStore.reportInfo?.metrikaItems">
<NFormItem :label="metrikaItem.name" :show-feedback="false">
<NInputNumber v-model:value="reportStore.reportForm[`metrika_item_${metrikaItem.metrika_item_id}`]"
:default-value="metrikaItem.default_value" />
</NFormItem>
</template>
</NFlex>
</NForm>
</NCard>
<NFlex v-else justify="space-between" align="center">
<NSpace>
<template v-for="metrikaItem in reportStore.reportInfo?.metrikaItems">
<NFormItem :label="metrikaItem.name" :show-feedback="false">
<NInputNumber v-model:value="reportStore.reportForm[`metrika_item_${metrikaItem.metrika_item_id}`]"
:default-value="metrikaItem.default_value" />
</NFormItem>
</template>
</NSpace>
</NFlex>
</NForm>
</NCard>
<NSpace :wrap="false">
<NCard class="min-w-[120px] max-w-[120px] min-h-[100px] max-h-[102px] h-full"
style="--n-padding-top: 0; --n-padding-left: 0; --n-padding-bottom: 0; --n-padding-right: 0;"
>
<div class="w-full h-full flex flex items-center justify-center">
<NStatistic label="Умерло" :value="reportStore.reportInfo?.department.deadCount" />
</div>
</NCard>
<NCard class="min-w-[120px] max-w-[120px] min-h-[100px] max-h-[102px] h-full"
style="--n-padding-top: 0; --n-padding-bottom: 0; --n-padding-left: 8px; --n-padding-right: 8px;">
<div class="w-full h-full flex flex items-center justify-center">
<NStatistic :value="reportStore.reportInfo?.department.surgicalCount[1]">
<template #label>
<div class="flex flex-col">
<span>Операций</span>
<span>Э / П</span>
</div>
</template>
<template #suffix>
/ {{ reportStore.reportInfo?.department.surgicalCount[0] }}
</template>
</NStatistic>
</div>
</NCard>
</NSpace>
</div>
</template>
<style scoped>
:deep(.n-statistic__label),
:deep(.n-statistic-value__content) {
@apply text-center;
}
:deep(.n-statistic-value) {
@apply flex justify-center items-center;
}
</style>

View File

@@ -7,6 +7,7 @@ import {ru} from "date-fns/locale";
import {useAuthStore} from "../../../Stores/auth.js";
import {storeToRefs} from "pinia";
import {RiAddCircleLine} from 'vue-icons-plus/ri'
import {TbAlertCircle} from 'vue-icons-plus/tb'
import {useReportStore} from "../../../Stores/report.js";
import ReportSelectDate from "../../../Components/ReportSelectDate.vue";
import DepartmentSelect from "../../../Components/DepartmentSelect.vue";
@@ -53,19 +54,16 @@ const currentDate = computed(() => {
<template>
<NCard>
<NFlex vertical>
<NFlex align="center" justify="space-between" :wrap="false">
<div class="grid grid-cols-[auto_1fr_auto] items-center">
<NTag v-if="isFillableMode" type="info" :bordered="false">
{{ authStore.userDepartment.name_full }}
</NTag>
<DepartmentSelect v-if="isReadonlyMode" />
<NFlex align="center" :wrap="false">
<NH2 v-if="isFillableMode" class="mb-0!">
{{ currentDate }}
</NH2>
<ReportSelectDate v-if="isReadonlyMode" />
</NFlex>
</NFlex>
<div class="col-3 w-full">
<ReportSelectDate />
</div>
</div>
<NFlex justify="space-between" align="center" :wrap="false">
<NRow class="grow">
@@ -113,9 +111,9 @@ const currentDate = computed(() => {
<NButton type="error" secondary @click="openUnwantedEventModal = true">
<template #icon>
<RiAddCircleLine />
<TbAlertCircle />
</template>
Нежелательное событие
Нежелательные события ({{ reportStore.unwantedEvents.length }})
</NButton>
</NFlex>
</NFlex>
@@ -125,5 +123,11 @@ const currentDate = computed(() => {
</template>
<style scoped>
:deep(.n-statistic__label),
:deep(.n-statistic-value__content) {
@apply text-center;
}
:deep(.n-statistic-value) {
@apply flex justify-center items-center;
}
</style>

View File

@@ -1,5 +1,5 @@
<script setup>
import {NCard, NFlex, NAlert, NCollapse, NCollapseItem} from 'naive-ui'
import {NCard, NFlex, NAlert, NCollapse, NCollapseItem, NTabPane, NTabs} from 'naive-ui'
import ReportSectionItem from "./ReportSectionItem.vue";
import {computed} from "vue";
import {useReportStore} from "../../../Stores/report.js";
@@ -79,12 +79,37 @@ const isReadonlyMode = computed(() => props.mode.toLowerCase() === 'readonly')
</NCollapseItem>
<NCollapseItem name="4">
<template #header>
<ReportSectionHeader title="Умершие" status="deceased" />
<ReportSectionHeader title="Выбывшие" status="outcome" />
</template>
<ReportSectionItem status="deceased"
@item-dragged="handleItemDragged"
@item-dropped="handleItemDropped"
/>
<NTabs type="segment" animated>
<NTabPane name="transferred">
<template #tab>
<ReportSectionHeader title="Переведённые" status="outcome-transferred" />
</template>
<ReportSectionItem status="outcome-transferred"
@item-dragged="handleItemDragged"
@item-dropped="handleItemDropped"
/>
</NTabPane>
<NTabPane name="discharged">
<template #tab>
<ReportSectionHeader title="Выписанные" status="outcome-discharged" />
</template>
<ReportSectionItem status="outcome-discharged"
@item-dragged="handleItemDragged"
@item-dropped="handleItemDropped"
/>
</NTabPane>
<NTabPane name="deceased">
<template #tab>
<ReportSectionHeader title="Умершие" status="outcome-deceased" />
</template>
<ReportSectionItem status="outcome-deceased"
@item-dragged="handleItemDragged"
@item-dropped="handleItemDropped"
/>
</NTabPane>
</NTabs>
</NCollapseItem>
</NCollapse>
</NCard>

View File

@@ -19,21 +19,17 @@ const reportStore = useReportStore()
const isLoading = ref(true)
const countPatient = ref(null)
const fetchPatientCount = async () => {
if (props.status === 'plan' || props.status === 'emergency') {
isLoading.value = true
const data = {
status: props.status,
startAt: reportStore.timestampCurrentRange[0],
endAt: reportStore.timestampCurrentRange[1]
}
await axios.post('/api/mis/patients/count', data).then((res) => {
countPatient.value = res.data
}).finally(() => {
isLoading.value = false
})
} else {
isLoading.value = false
isLoading.value = true
const data = {
status: props.status,
startAt: reportStore.timestampCurrentRange[0],
endAt: reportStore.timestampCurrentRange[1]
}
await axios.post('/api/mis/patients/count', data).then((res) => {
countPatient.value = res.data
}).finally(() => {
isLoading.value = false
})
}
const computedHeader = computed(() => {

View File

@@ -1,10 +1,11 @@
<script setup>
import {NIcon, NText, NDataTable, NButton} from "naive-ui";
import {NIcon, NText, NDataTable, NButton, NTabs, NTabPane} from "naive-ui";
import {useReportStore} from "../../../Stores/report.js";
import {computed, h, onMounted, ref, watch} from "vue";
import { VueDraggableNext } from 'vue-draggable-next'
import {storeToRefs} from "pinia";
import {TbGripVertical} from "vue-icons-plus/tb";
import MoveModalComment from "./MoveModalComment.vue";
const props = defineProps({
mode: {
@@ -30,7 +31,7 @@ const props = defineProps({
accentIds: {
type: Array,
default: []
}
},
})
const isFillableMode = computed(() => props.mode.toLowerCase() === 'fillable')
@@ -46,10 +47,12 @@ const {patientsData} = storeToRefs(reportStore)
const baseColumns = reportStore.getColumnsByKey(props.keys)
const data = ref([])
const isLoading = ref(true)
const showMoveModal = ref(false)
const latestDropItem = ref(null)
// Добавляем drag колонку если режим fillable
const columns = computed(() => {
if (!isFillableMode.value) return baseColumns
// if (!isFillableMode.value) return baseColumns
const newColumns = []
@@ -83,7 +86,11 @@ const columns = computed(() => {
{
text: true,
onClick: () => {
alert('message')
axios.post('/api/report/observation/remove', {
id: row.id
}).then(async () => {
await fetchPatients()
})
}
},
[
@@ -92,11 +99,30 @@ const columns = computed(() => {
)
}
const expandColumn = {
type: 'expand',
renderExpand: (rowData) => {
return h(
NText,
{
class: 'max-w-full break-words whitespace-normal'
},
{
default: rowData.comment ?? 'Причина наблюдения не указана'
}
)
}
}
if (props.status === 'observation') {
newColumns.push(expandColumn)
}
if (props.isDraggable) newColumns.push(dragColumn)
newColumns.push(...baseColumns)
if (props.isRemovable) newColumns.push(removeColumn)
if (props.status === 'emergency') {
if (props.status === 'emergency' || props.status === 'plan') {
const operationColumn = {
title: 'Операции',
key: 'operations',
@@ -109,11 +135,25 @@ const columns = computed(() => {
return `${itm.code}; `
})
]
) : h('div', {}, '-')
) : h('div', {}, '-'),
ellipsis: {
tooltip: true
}
}
newColumns.push(operationColumn)
}
if (props.status === 'outcome') {
const typeColumn = {
title: 'Причина',
key: 'outcome_type',
ellipsis: {
tooltip: true
}
}
newColumns.push(typeColumn)
}
return newColumns
})
@@ -156,6 +196,9 @@ const handleDrop = (e) => {
fromStatus: dragData.fromStatus,
toStatus: props.status
})
latestDropItem.value = dragData.row
showMoveModal.value = true
} catch (error) {
console.error('Drop error:', error)
}
@@ -216,24 +259,21 @@ onMounted(async () => {
</script>
<template>
<NDataTable :columns="columns"
ref="tableRef"
:data="patientsData[status]"
size="small"
@drop="handleDrop"
@dragover="handleDragOver"
:loading="isLoading"
max-height="200"
min-height="200"
:row-props="rowProps"
class="text-sm!">
</NDataTable>
<!-- <NDataTable :columns="columns"-->
<!-- :data="data"-->
<!-- size="small"-->
<!-- max-height="200"-->
<!-- class="text-sm!">-->
<!-- </NDataTable>-->
<NDataTable :columns="columns"
ref="tableRef"
:data="patientsData[status]"
size="small"
@drop="handleDrop"
@dragover="handleDragOver"
:loading="isLoading"
max-height="200"
min-height="200"
:row-props="rowProps"
:row-key="(row) => row.id"
class="text-sm!">
</NDataTable>
<MoveModalComment v-model:show="showMoveModal" :patient-id="latestDropItem?.id" />
</template>
<style scoped>

View File

@@ -1,12 +1,16 @@
<script setup>
import {NModal, NForm, NFormItem, NInput, NFlex, NButton} from 'naive-ui'
import {useForm} from "@inertiajs/vue3";
import {NModal, NList, NListItem, NThing, NAvatar, NIcon, NDrawer, NDrawerContent,
NText, NDivider, NForm, NFormItem, NInput, NFlex, NButton, NScrollbar, NEmpty
} from 'naive-ui'
import {useReportStore} from "../../../Stores/report.js";
import {ref} from "vue";
const open = defineModel('open')
import { TbAlertCircle, TbPencil, TbTrashX, TbCirclePlus, TbCheck, TbX } from 'vue-icons-plus/tb'
import {format, isValid} from "date-fns";
const reportStore = useReportStore()
const formRef = ref()
const createDrawerShow = ref(false)
const rules = {
comment: {
required: true,
@@ -14,34 +18,191 @@ const rules = {
trigger: 'blur'
}
}
const selectedEvent = ref(null)
const drawerCreatingMode = ref(true) // or false = editing
const onSubmit = () => {
// Создание в сторе и открытие drawer с формой нежелательного события
const onCreateEvent = () => {
drawerCreatingMode.value = true
const createDate = format(new Date(), 'Создано dd.MM.yyyy в HH:mm')
reportStore.unwantedEvents.push({
title: `Нежелательное событие №${reportStore.unwantedEvents.length + 1}`,
comment: '',
created_at: createDate
})
const length = reportStore.unwantedEvents.length
selectedEvent.value = reportStore.unwantedEvents[length - 1]
createDrawerShow.value = true
}
const onEditEvent = (event) => {
drawerCreatingMode.value = false
selectedEvent.value = event
createDrawerShow.value = true
}
const onDeleteEvent = (event) => {
const indexOfDelete = reportStore.unwantedEvents.findIndex(itm => itm === event)
if (typeof event.unwanted_event_id !== 'undefined') {
axios.delete(`/api/report/unwanted-event/${event.unwanted_event_id}`)
.then(() => {
reportStore.unwantedEvents.splice(indexOfDelete, 1)
})
} else {
reportStore.unwantedEvents.splice(indexOfDelete, 1)
}
}
const onCancelDrawerEvent = (event) => {
onDeleteEvent(event)
createDrawerShow.value = false
}
const onCreateDrawerEvent = () => {
formRef.value?.validate((errors) => {
if (!errors) {
open.value = false
console.log(createDrawerShow.value)
createDrawerShow.value = false
console.log(createDrawerShow.value)
}
else {
}
})
}
const onBeforeLeaveModal = () => {
selectedEvent.value = null
drawerCreatingMode.value = true
createDrawerShow.value = false
}
</script>
<template>
<NModal v-model:show="open" title="Нежелательное событие" preset="card" class="max-w-xl">
<NForm ref="formRef" :model="reportStore.reportForm" :rules="rules">
<NFormItem :show-label="false" path="comment">
<NInput type="textarea" :rows="8" v-model:value="reportStore.reportForm.comment" />
</NFormItem>
</NForm>
<NModal v-model:show="open"
title="Нежелательные события"
preset="card"
:mask-closable="false"
:close-on-esc="false"
@before-leave="onBeforeLeaveModal"
class="max-w-4xl overflow-clip h-[calc(100vh-220px)]"
>
<template v-if="reportStore.unwantedEvents.length">
<NScrollbar class="max-h-[calc(100vh-282px)] pr-3">
<NList>
<NListItem v-for="event in reportStore.unwantedEvents">
<NThing>
<template #avatar>
<NAvatar>
<NIcon>
<TbAlertCircle class="text-red-400" />
</NIcon>
</NAvatar>
</template>
<template #header>
{{ event.title }}
</template>
<template #description>
<NText depth="3">
{{ event.created_at }}
</NText>
</template>
<NText>
{{ event.comment }}
</NText>
<template #action>
<NFlex align="center">
<NButton secondary size="small" @click="onEditEvent(event)">
<template #icon>
<TbPencil />
</template>
Редактировать
</NButton>
<NDivider vertical />
<NButton type="error" secondary size="small" @click="onDeleteEvent(event)">
<template #icon>
<TbTrashX />
</template>
Удалить
</NButton>
</NFlex>
</template>
</NThing>
</NListItem>
</NList>
</NScrollbar>
</template>
<template v-else>
<div class="h-full flex items-center justify-center">
<NEmpty description="Нежелательных событий не найдено!">
<template #extra>
<NButton type="primary" secondary @click="onCreateEvent()" size="small">
<template #icon>
<TbCirclePlus />
</template>
Создать
</NButton>
</template>
</NEmpty>
</div>
</template>
<template #action>
<NFlex align="center" justify="end">
<NButton type="primary" tertiary @click="onSubmit">
Сохранить
<NFlex id="modal-action" align="center" justify="space-between">
<NButton type="primary" secondary @click="onCreateEvent()">
<template #icon>
<TbCirclePlus />
</template>
Создать событие
</NButton>
</NFlex>
</template>
<NDrawer
:show="createDrawerShow"
placement="bottom"
:max-height="600"
:min-height="400"
:default-height="400"
resizable
:trap-focus="false"
:block-scroll="false"
:mask-closable="false"
to="#modal-action"
>
<NDrawerContent>
<template #header>
<template v-if="drawerCreatingMode">Создание события</template>
<template v-else>Редактирование события</template>
</template>
<NForm ref="formRef" :model="selectedEvent" :rules="rules">
<NFormItem :show-label="false" path="comment">
<NInput type="textarea" :rows="8" v-model:value="selectedEvent.comment" />
</NFormItem>
</NForm>
<template #footer>
<NFlex align="center">
<NButton v-if="drawerCreatingMode" type="error" secondary @click="onCancelDrawerEvent(selectedEvent)">
<template #icon>
<TbX />
</template>
Отменить создание
</NButton>
<NDivider v-if="drawerCreatingMode" vertical />
<NButton type="primary" @click="onCreateDrawerEvent">
<template #icon>
<TbCheck />
</template>
<template v-if="drawerCreatingMode">Создать</template>
<template v-else>Сохранить</template>
</NButton>
</NFlex>
</template>
</NDrawerContent>
</NDrawer>
</NModal>
</template>