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

Переписаны запросы для статистики, отчетов
Добавлена интеграция отчета сестры
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

@@ -0,0 +1,287 @@
<script setup>
import PatientTypeSection from "./PatientSection.vue";
import PatientDataTable from "./PatientDataTable.vue";
import {NButton, NFlex, NInput, NTabPane, NTabs, NSpace, NDivider} from "naive-ui";
import PatientTypeSectionItem from "./PatientSectionItem.vue";
import {computed, inject, ref} from "vue";
import {usePatientColumns} from "../../../Composables/usePatientColumns.js";
import OperationInfoModal from "./OperationInfoModal.vue";
import ObservableModal from "./Modals/ObservableModal.vue";
import {useAppDialog} from "../../../Composables/useAppDialog.js";
import {TbSearch} from "vue-icons-plus/tb";
import {router, usePage} from "@inertiajs/vue3";
const props = defineProps({
patients: {
type: Object,
default: () => ({data: []})
},
nursePatients: {
type: Object,
default: () => ({data: []})
}
})
const emits = defineEmits([
'createObservable',
'removeObservable',
])
const operationsInModal = ref(null)
const showOperationsModal = ref(false)
const activePatient = ref(null)
const showObservableModal = ref(false)
const searchArg = ref(null)
const searching = ref(false)
const { reportForm, updateReportForm } = inject('reportForm')
const page = usePage()
const canEditObservable = computed(() => {
const perms = page.props.user?.permissions ?? []
return perms.includes('report.create') || perms.includes('report.edit') || perms.includes('report.edit.past')
})
// Нормализация даты до минут для сравнения (убирает расхождения в секундах)
const normDate = (d) => {
if (!d) return null
try { return new Date(d).toISOString().slice(0, 16) } catch { return null }
}
const nurseByOriginalId = computed(() => {
const map = new Map()
for (const p of props.nursePatients?.data ?? []) {
if (p.original_id != null) map.set(p.original_id, p)
}
return map
})
const FIELD_LABELS = {
recipient_date: 'дата госпитализации',
extract_date: 'дата выписки',
death_date: 'дата смерти',
ingoing_date: 'дата поступления в отделение',
out_date: 'дата выбытия из отделения',
department: 'перевод в другое отделение',
}
const getNurseChanges = (misPat) => {
// Текущий период: MedicalHistory использует 'id', прошлый: ReportDutyPatient — 'original_id'
const patientKey = misPat.original_id ?? misPat.id
const nurseP = nurseByOriginalId.value.get(patientKey)
if (!nurseP) return []
const changes = []
if (normDate(nurseP.recipient_date) !== normDate(misPat.recipient_date))
changes.push('recipient_date')
if (normDate(nurseP.extract_date) !== normDate(misPat.extract_date))
changes.push('extract_date')
if (normDate(nurseP.death_date) !== normDate(misPat.death_date))
changes.push('death_date')
// Текущий период грузит latestMigration (объект), прошлый — migrations (массив)
const nurseMig = nurseP.migrations?.[0]
const misMig = misPat.migrations?.[0] ?? misPat.latest_migration
if (nurseMig && misMig) {
if (normDate(nurseMig.ingoing_date) !== normDate(misMig.ingoing_date))
changes.push('ingoing_date')
if (normDate(nurseMig.out_date) !== normDate(misMig.out_date))
changes.push('out_date')
if (nurseMig.department_id !== misMig.department_id)
changes.push('department')
}
return changes
}
const withNurse = (p) => {
const nurse_changes = getNurseChanges(p)
if (!nurse_changes.length) return p
return {
...p,
nurse_changes,
nurse_changes_labels: nurse_changes.map(k => FIELD_LABELS[k] ?? k),
}
}
const patientsByGroup = computed(() => {
const groups = {
urgent: [],
planned: [],
deceased: [],
in_department: [],
recipient: [],
discharged: [],
transferred: [],
reanimations: [],
observables: []
}
const patients = props.patients?.data ?? []
for (const raw of patients) {
const p = withNurse(raw)
const flags = p.period_flags ?? {}
const isCurrentAtEnd = flags.current_at_end ?? ['in_department', 'recipient'].includes(p.patient_status)
// Группировка по срочности за период: пациент должен состоять на конец периода.
if (isCurrentAtEnd && (flags.urgent ?? p.patient_urgency === 'urgent')) groups.urgent.push(p)
else if (isCurrentAtEnd && (flags.planned ?? p.patient_urgency === 'planned')) groups.planned.push(p)
// Группировка по реанимации
if (p.in_reanimation === true) groups.reanimations.push(p)
// Группировка по наблюдению
if (p.in_observable === true) groups.observables.push(p)
// Событийная группировка за период. Один пациент может быть в нескольких группах.
if (flags.recipient ?? p.patient_status === 'recipient') groups.recipient.push(p)
if (flags.current_at_end ?? p.patient_status === 'in_department') groups.in_department.push(p)
if (flags.discharged ?? p.patient_status === 'discharged') groups.discharged.push(p)
if (flags.deceased ?? p.patient_status === 'deceased') groups.deceased.push(p)
if (flags.transferred ?? p.patient_status === 'transferred') groups.transferred.push(p)
}
return groups
})
const {
planOrEmergencyColumns, observableColumns, reanimationColumns, dischargedColumns,
deceasedColumns, transferredColumns,
} = usePatientColumns({
onAddObservable: (row) => {
activePatient.value = row
showObservableModal.value = true
},
onShowOperationModal: ({patient, operations}) => {
activePatient.value = patient
operationsInModal.value = operations
showOperationsModal.value = true
},
onAddObservableComment: (row) => {
activePatient.value = row
showObservableModal.value = true
},
onRemoveObservable: async (row) => {
await useAppDialog({
title: 'Снятие с контроля',
content: 'Это действие необратимо. Продолжить?',
onConfirm: async () => {
const queryString = window.location.search;
const params = new URLSearchParams(queryString);
const url = new URL(`${window.location.origin}/api/duty/observable/close`)
url.searchParams.append('startAt', params.get('startAt'))
url.searchParams.append('endAt', params.get('endAt'))
try {
await axios.post(url.toString(), { ...row })
} catch (e) {
if (e.response?.status !== 404) throw e
// 404 — запись не найдена, удаляем локально
}
emits('removeObservable', row)
}
})
}
}, { canEditObservable })
const findPatient = () => {
if (searching.value) return
// Просто перезагружаем текущую страницу с новым search параметром
router.reload({
data: {
search: searchArg.value || null, // Отправляем как POST или GET параметр
},
preserveState: true,
preserveScroll: true,
only: ['patients', 'search'], // Обновляем только нужные пропсы
onStart: () => {
searching.value = true
},
onFinish: () => {
searching.value = false
}
})
}
const resetSearch = () => {
searchArg.value = null
router.reload({
data: { search: null }, // Отправляем null
preserveState: true,
preserveScroll: true,
only: ['patients', 'search', 'meta'],
onStart: () => {
searching.value = true
},
onFinish: () => {
searching.value = false
}
});
}
const onObservablePatient = (model) => {
const observableKey = 'observables'
const observablesInReport = reportForm.value[observableKey]
const preparedModel = {
...model,
in_observable: true
}
const form = { [observableKey]: [...observablesInReport, preparedModel] }
updateReportForm(form)
emits('createObservable', preparedModel)
}
</script>
<template>
<NSpace vertical :size="16">
<NFlex :wrap="false">
<NInput v-model:value="searchArg"
placeholder="Поиск пациента по ФИО"
@keydown.enter="findPatient"
clearable
@clear="resetSearch"
/>
<NButton secondary @click="findPatient">
<template #icon>
<TbSearch />
</template>
Найти
</NButton>
</NFlex>
<PatientTypeSection>
<PatientTypeSectionItem label="Планово" :counter="patientsByGroup.planned.length">
<PatientDataTable :data="patientsByGroup.planned" :columns="planOrEmergencyColumns" :loading="searching" />
</PatientTypeSectionItem>
<PatientTypeSectionItem label="Экстренно" :counter="patientsByGroup.urgent.length">
<PatientDataTable :data="patientsByGroup.urgent" :columns="planOrEmergencyColumns" :loading="searching" />
</PatientTypeSectionItem>
<PatientTypeSectionItem label="Находятся на контроле" :counter="patientsByGroup.observables.length">
<PatientDataTable :data="patientsByGroup.observables" :columns="observableColumns" :loading="searching" />
</PatientTypeSectionItem>
<PatientTypeSectionItem label="Находятся в реанимации" :counter="patientsByGroup.reanimations.length">
<PatientDataTable :data="patientsByGroup.reanimations" :columns="reanimationColumns" :loading="searching" />
</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" :loading="searching" />
</NTabPane>
<NTabPane name="2" :tab="`Умершие (${patientsByGroup.deceased.length})`">
<PatientDataTable :data="patientsByGroup.deceased" :columns="deceasedColumns" :loading="searching" />
</NTabPane>
<NTabPane name="3" :tab="`Переведенные (${patientsByGroup.transferred.length})`">
<PatientDataTable :data="patientsByGroup.transferred" :columns="transferredColumns" :loading="searching" />
</NTabPane>
</NTabs>
</PatientTypeSectionItem>
</PatientTypeSection>
</NSpace>
<OperationInfoModal :patient="activePatient" :operations="operationsInModal" v-model:show="showOperationsModal" />
<ObservableModal :patient="activePatient" v-model:show="showObservableModal" @on-submit="(model) => onObservablePatient(model)" />
</template>
<style scoped>
</style>