Обновлен стартовый экран

Переписаны запросы для статистики, отчетов
Добавлена интеграция отчета сестры
This commit is contained in:
brusnitsyn
2026-05-28 22:10:00 +09:00
parent 90e0d04dfd
commit 739168d427
96 changed files with 6663 additions and 1465 deletions

View File

@@ -1,35 +1,29 @@
<script setup>
import AppLayout from "../../Layouts/AppLayout.vue";
import {useReportStore} from "../../Stores/report.js";
import {computed, h, onMounted, ref, watch} from "vue";
import {onMounted, ref, watch, provide} from "vue";
import {useAuthStore} from "../../Stores/auth.js";
import {
NFormItem,
NFlex,
NInputNumber,
NStatistic,
NTabPane,
NTabs,
NTag,
NNumberAnimation,
NSkeleton,
NRow, NCol, NDivider, NButton
NRow, NCol, NDivider, NButton, NSpace
} from "naive-ui";
import DatePickerQuery from "../../Components/DatePickerQuery.vue";
import AppPanel from "../../Components/AppPanel.vue";
import AppContainer from "../../Components/AppContainer.vue";
import PatientTypeSection from "./Components/PatientSection.vue";
import PatientTypeSectionItem from "./Components/PatientSectionItem.vue";
import PatientDataTable from "./Components/PatientDataTable.vue";
import {format, formatDistanceStrict} from 'date-fns';
import { ru } from 'date-fns/locale';
import TooltipColumn from "./Components/DataTableColumns/TooltipColumn.vue";
import ActionsColumn from "./Components/DataTableColumns/ActionsColumn.vue";
import OperationsColumn from "./Components/DataTableColumns/OperationsColumn.vue";
import ReportWidget from "./Components/ReportWidget.vue";
import OperationInfoModal from "./Components/OperationInfoModal.vue";
import {usePatientColumns} from "../../Composables/usePatientColumns.js";
import {router} from "@inertiajs/vue3";
import HeaderWidget from "./Components/HeaderWidget.vue";
import DutyPatientsPane from "./Components/DutyPatientsPane.vue";
import NursePatientsPane from "./Components/NursePatientsPane.vue";
import OperationWidget from "./Components/Widgets/OperationWidget.vue";
import LoadedWidget from "./Components/Widgets/LoadedWidget.vue";
import UnwantedEventModal from "./Components/Modals/UnwantedEventModal.vue";
import RecipientWidget from "./Components/Widgets/RecipientWidget.vue";
const props = defineProps({
department: {
@@ -44,75 +38,154 @@ const props = defineProps({
type: Array,
default: []
},
canSaveReport: Boolean,
canEditPastReport: Boolean,
canSaveNurseReport: Boolean,
stats: {
type: Object,
default: () => ({
duty: {
beds: 0,
loaded: 0,
current: 0,
urgent: 0,
discharged: 0,
},
nurse: {
current: 0,
urgent: 0,
discharged: 0,
},
})
},
latestReport: {
type: Object,
default: () => ({ })
},
patients: {
type: Array,
default: () => ([])
},
nursePatients: {
type: Array,
default: () => ([])
},
dates: {
type: Array,
default: []
},
selectedUserId: {
type: Number,
default: null
},
selectedDepartmentId: {
type: Number,
default: null
}
})
const reportStore = useReportStore()
const authStore = useAuthStore()
const userDepartment = authStore.userDepartment
const loading = ref(false)
const operationsInModal = ref(null)
const showOperationsModal = ref(false)
const patientsByGroup = computed(() => {
const groups = {
urgent: [],
planned: [],
deceased: [],
in_department: [],
recipient: [],
discharged: [],
transferred: [],
};
for (const p of props.patients.data) {
// Группировка по срочности
if (p.patient_urgency === 'urgent') groups.urgent.push(p);
else if (p.patient_urgency === 'planned') groups.planned.push(p);
// Группировка по статусу (дублирование нужно, если один пациент может быть в двух таблицах)
if (groups.hasOwnProperty(p.patient_status)) {
groups[p.patient_status].push(p);
}
}
return groups;
const patientCollection = ref({...props.patients})
const nursePatientCollection = ref({...props.nursePatients})
const latestReportObj = ref(props.latestReport ?? {
unwanted_events: []
})
const {
planOrEmergencyColumns, observableColumns, reanimationColumns, dischargedColumns,
deceasedColumns, transferredColumns,
} = usePatientColumns({
onAddObservable: (row) => {
console.log(row)
},
onShowOperationModal: (operations) => {
operationsInModal.value = operations
showOperationsModal.value = true
const loading = ref(false)
const showUnwantedEventModal = ref(false)
const reportForm = ref({
recipient: props.stats.duty.recipient,
discharged: props.stats.duty.discharged,
current: props.stats.duty.current,
staff: 0,
observables: []
})
const updateReportForm = (form) => {
reportForm.value = {
...reportForm.value,
...form
}
}
const updateUnwantedEvents = (unwanted_events) => {
reportForm.value = {
...reportForm.value,
unwanted_events
}
latestReportObj.value = {
...latestReportObj.value,
unwanted_events
}
}
// Прокидываем reportForm всем потомкам
provide('reportForm', {
reportForm,
updateReportForm
})
const submit = () => {
router.post('/duty/report/save', {}, {
const queryString = window.location.search;
const params = new URLSearchParams(queryString);
const url = new URL(`${window.location.origin}/duty/report/save`)
const startAt = params.get('startAt')
const endAt = params.get('endAt')
if (startAt && startAt !== 'null') url.searchParams.append('startAt', startAt)
if (endAt && endAt !== 'null') url.searchParams.append('endAt', endAt)
router.post(url.toString(), {
...reportForm.value,
userId: props.selectedUserId,
departmentId: props.selectedDepartmentId,
}, {
onSuccess: () => {
alert('Сохранено')
}
})
}
const syncPageProps = () => reportStore.initializeFromPage(props)
const onShowUnwantedEventModal = () => {
showUnwantedEventModal.value = true
}
const addObservablePatient = (observable) => {
const collection = patientCollection.value.data
const item = collection.find(obs => obs.id === observable.id)
if (item) {
item.in_observable = true
item.observable = { observable_reason: observable.observable_reason }
}
}
const removeObservablePatient = (patient) => {
const collection = patientCollection.value.data
const item = collection.find(p => p.id === patient.id)
if (item) {
item.in_observable = false
}
}
const syncPageProps = (pageProps = props) => {
reportStore.initializeFromPage(pageProps)
patientCollection.value = {...pageProps.patients}
nursePatientCollection.value = {...pageProps.nursePatients}
latestReportObj.value = pageProps.latestReport ?? { unwanted_events: [] }
reportForm.value = {
...reportForm.value,
recipient: pageProps.stats.duty.recipient,
discharged: pageProps.stats.duty.discharged,
current: pageProps.stats.duty.current,
}
}
onMounted(syncPageProps)
watch(() => props, (newProps) => {
reportStore.initializeFromPage(newProps)
syncPageProps(newProps)
}, {
deep: true,
immediate: true
@@ -129,168 +202,100 @@ watch(() => props, (newProps) => {
<AppContainer>
<AppPanel>
<NFlex justify="space-between" align="center">
<NTag type="info" :bordered="false">
{{ userDepartment.name_full }}
</NTag>
<NSpace>
<NTag type="info" :bordered="false">
{{ department.name_full }}
</NTag>
<NTag v-if="props.latestReport" type="warning" :bordered="false">
Отчет создан: {{ `${props.latestReport.doctor.FAM_V} ${props.latestReport.doctor.IM_V} ${props.latestReport.doctor.OT_V}` }}
</NTag>
</NSpace>
<DatePickerQuery :date="dates" :is-head-or-admin="true" class="text-lg!" />
</NFlex>
<NDivider dashed class="my-4! mt-3!" />
<NRow class="grow space-x-2">
<NRow class="grow space-x-2 h-[82.78px]">
<NCol :span="3">
<AppPanel no-padding>
<div class="flex flex-col items-center justify-center text-center py-2">
<NStatistic label="Коек">
<template #default>
<NSkeleton v-if="reportStore.isLoadReportInfo" round class="w-[70px]! mt-2 h-[29px]!" />
<NNumberAnimation v-else :from="0" :to="reportStore.reportInfo?.department?.beds" />
</template>
</NStatistic>
</div>
</AppPanel>
<HeaderWidget label="Коек"
:counter="stats.duty.beds" />
</NCol>
<NCol :span="3">
<AppPanel no-padding>
<div class="flex flex-col items-center justify-center text-center py-2">
<NStatistic label="Загруженность">
<template #default>
<NNumberAnimation :from="0" :to="reportStore.reportInfo?.department?.percentLoadedBeds" />
<span>%</span>
</template>
</NStatistic>
</div>
</AppPanel>
<LoadedWidget :counter="stats.duty.loaded" />
</NCol>
<NCol :span="3">
<AppPanel no-padding>
<div class="flex flex-col items-center justify-center text-center py-2">
<NStatistic label="Состоит">
<NNumberAnimation :from="0" :to="patientsByGroup.in_department.length + patientsByGroup.recipient.length" />
</NStatistic>
</div>
</AppPanel>
<RecipientWidget :beds="stats.duty.beds"
:duty-current="stats.duty.current"
:nurse-current="stats.nurse.current"
/>
</NCol>
<NCol :span="3">
<AppPanel no-padding>
<div class="flex flex-col items-center justify-center text-center py-2">
<NStatistic label="Поступило">
<NNumberAnimation :from="0" :to="patientsByGroup.recipient.length" />
</NStatistic>
</div>
</AppPanel>
<HeaderWidget label="Поступило"
is-double-counter
:counter="stats.duty.recipient"
:counter-suffix="stats.nurse.recipient"
/>
</NCol>
<NCol :span="3">
<AppPanel no-padding>
<div class="flex flex-col items-center justify-center text-center py-2">
<NStatistic label="Выбыло">
<NNumberAnimation :from="0" :to="patientsByGroup.deceased.length + patientsByGroup.discharged.length" />
</NStatistic>
</div>
</AppPanel>
<HeaderWidget label="Выбыло"
is-double-counter
:counter="stats.duty.discharged"
:counter-suffix="stats.nurse.discharged"
/>
</NCol>
</NRow>
</AppPanel>
<div class="grid grid-cols-[1fr_auto] gap-x-2">
<AppPanel>
<NFlex align="center" :wrap="false">
<NFormItem label="Поступило" :show-feedback="false">
<NInputNumber :min="0" />
<NInputNumber :disabled="!canSaveReport" :min="0" v-model:value="reportForm.recipient" />
</NFormItem>
<NFormItem label="Выбыло" :show-feedback="false">
<NInputNumber :min="0" />
<NInputNumber :disabled="!canSaveReport" :min="0" v-model:value="reportForm.discharged" />
</NFormItem>
<NFormItem label="Состоит" :show-feedback="false">
<NInputNumber :min="0" />
<NInputNumber :disabled="!canSaveReport" :min="0" v-model:value="reportForm.current" />
</NFormItem>
<NFormItem label="Мед. персонал" :show-feedback="false">
<NInputNumber :min="0" />
<NInputNumber :disabled="!canSaveReport" :min="0" v-model:value="reportForm.staff" />
</NFormItem>
</NFlex>
</AppPanel>
<NFlex :wrap="false" :size="8">
<ReportWidget :to="patientsByGroup.deceased.length">
<ReportWidget :counter="stats.duty.deceased">
Умерло
</ReportWidget>
<ReportWidget :to="patientsByGroup.deceased.length">
<NSpace vertical :size="0">
<ReportWidget :counter="stats.duty.deceased">
<NSpace vertical :size="1">
<div>Летальность</div>
<div>%</div>
</NSpace>
</ReportWidget>
<ReportWidget :to="patientsByGroup.deceased.length">
<NSpace vertical :size="0">
<div>Операций</div>
<div>Э / П</div>
</NSpace>
</ReportWidget>
<OperationWidget :planned="stats.duty.surgical_planned" :urgent="stats.duty.surgical_urgent" />
</NFlex>
</div>
<AppPanel>
<NTabs type="segment">
<NTabPane name="mis" tab="МИС">
<PatientTypeSection>
<PatientTypeSectionItem label="Планово" :counter="patientsByGroup.planned.length">
<PatientDataTable :data="patientsByGroup.planned" :columns="planOrEmergencyColumns" />
</PatientTypeSectionItem>
<PatientTypeSectionItem label="Экстренно" :counter="patientsByGroup.urgent.length">
<PatientDataTable :data="patientsByGroup.urgent" :columns="planOrEmergencyColumns" />
</PatientTypeSectionItem>
<PatientTypeSectionItem label="Находятся на контроле" :counter="patientsByGroup.recipient.length">
<PatientDataTable :data="patientsByGroup.recipient" :columns="observableColumns" />
</PatientTypeSectionItem>
<PatientTypeSectionItem label="Находятся в реанимации" :counter="patientsByGroup.recipient.length">
<PatientDataTable :data="patientsByGroup.recipient" :columns="reanimationColumns" />
</PatientTypeSectionItem>
<PatientTypeSectionItem label="Выбывшие" :counter="patientsByGroup.discharged.length + patientsByGroup.deceased.length">
<NTabs type="segment" animated>
<NTabPane name="1" :tab="`Выписанные (${patientsByGroup.discharged.length})`">
<PatientDataTable :data="patientsByGroup.discharged" :columns="dischargedColumns" />
</NTabPane>
<NTabPane name="2" :tab="`Умершие (${patientsByGroup.deceased.length})`">
<PatientDataTable :data="patientsByGroup.deceased" :columns="deceasedColumns" />
</NTabPane>
<NTabPane name="3" :tab="`Переведенные (${patientsByGroup.transferred.length})`">
<PatientDataTable :data="patientsByGroup.transferred" :columns="transferredColumns" />
</NTabPane>
</NTabs>
</PatientTypeSectionItem>
</PatientTypeSection>
</NTabPane>
<NTabPane name="nurse" tab="Мед. сестра">
<PatientTypeSection>
<PatientTypeSectionItem label="Планово" :counter="patientsByGroup.planned.length">
<PatientDataTable :data="patientsByGroup.planned" :columns="planOrEmergencyColumns" />
</PatientTypeSectionItem>
<PatientTypeSectionItem label="Экстренно" :counter="patientsByGroup.urgent.length">
<PatientDataTable :data="patientsByGroup.urgent" :columns="planOrEmergencyColumns" />
</PatientTypeSectionItem>
<PatientTypeSectionItem label="Находятся на контроле" :counter="patientsByGroup.recipient.length">
<PatientDataTable :data="patientsByGroup.recipient" :columns="observableColumns" />
</PatientTypeSectionItem>
<PatientTypeSectionItem label="Находятся в реанимации" :counter="patientsByGroup.recipient.length">
<PatientDataTable :data="patientsByGroup.recipient" :columns="reanimationColumns" />
</PatientTypeSectionItem>
<PatientTypeSectionItem label="Выбывшие" :counter="patientsByGroup.discharged.length + patientsByGroup.deceased.length">
<NTabs type="segment" animated>
<NTabPane name="1" :tab="`Выписанные (${patientsByGroup.discharged.length})`">
<PatientDataTable :data="patientsByGroup.discharged" :columns="dischargedColumns" />
</NTabPane>
<NTabPane name="2" :tab="`Умершие (${patientsByGroup.deceased.length})`">
<PatientDataTable :data="patientsByGroup.deceased" :columns="deceasedColumns" />
</NTabPane>
<NTabPane name="3" :tab="`Переведенные (${patientsByGroup.transferred.length})`">
<PatientDataTable :data="patientsByGroup.transferred" :columns="transferredColumns" />
</NTabPane>
</NTabs>
</PatientTypeSectionItem>
</PatientTypeSection>
</NTabPane>
</NTabs>
<NTabs type="segment">
<NTabPane name="mis" tab="МИС" display-directive="show">
<DutyPatientsPane :patients="patientCollection" :nurse-patients="nursePatientCollection" @create-observable="addObservablePatient" @remove-observable="removeObservablePatient" />
</NTabPane>
<NTabPane name="nurse" tab="Журнал пациентов" display-directive="show">
<NursePatientsPane :patients="nursePatientCollection" />
</NTabPane>
</NTabs>
</AppPanel>
<AppPanel>
<NFlex justify="space-between">
<NButton v-if="props.canSaveReport || props.canEditPastReport"
secondary type="error" :loading="loading" @click="onShowUnwantedEventModal">
Нежелательные события ({{ latestReportObj.unwanted_events?.length ?? 0 }})
</NButton>
<NButton v-if="props.canSaveReport" secondary @click="submit" :loading="loading">
Сохранить отчет
</NButton>
</NFlex>
</AppPanel>
<NButton secondary size="large" @click="submit" :loading="loading">
Сохранить отчет
</NButton>
</AppContainer>
<UnwantedEventModal v-model:show="showUnwantedEventModal" :can-save-report="props.canSaveReport" :report="latestReportObj" @onSubmit="updateUnwantedEvents" />
</AppLayout>
<OperationInfoModal :operations="operationsInModal" v-model:show="showOperationsModal" />
</template>