Обновлен стартовый экран
Переписаны запросы для статистики, отчетов Добавлена интеграция отчета сестры
This commit is contained in:
287
resources/js/Pages/Report/Components/DutyPatientsPane.vue
Normal file
287
resources/js/Pages/Report/Components/DutyPatientsPane.vue
Normal 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>
|
||||
Reference in New Issue
Block a user