Files
onboard/resources/js/Pages/Report/Components/DutyPatientsPane.vue
brusnitsyn 739168d427 Обновлен стартовый экран
Переписаны запросы для статистики, отчетов
Добавлена интеграция отчета сестры
2026-05-28 22:10:00 +09:00

288 lines
12 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 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>