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