* добавил исход спец контингенту

* оптимизация обновления при редактировании спец контингента
* добавил поддержку заключительных диагнозов
* изменил определение законченной операции
* добавил поддержку исхода операции
* добавил определение отмены для операции через назначение
* работа над диапазонами календарей, подсчет статистики
* добавил статусы отчетов и подкорректировал привязку спец контингента к отчету
* добавил новые сервисы для будущего кеширования
* частичное разделение логики подсчета пациентов
This commit is contained in:
brusnitsyn
2026-04-22 20:35:39 +09:00
parent 2041ab54ea
commit 719eb1403f
39 changed files with 1458 additions and 763 deletions

View File

@@ -0,0 +1,19 @@
<script setup>
import { NGrid } from 'naive-ui'
const props = defineProps({
cols: {
type: Number,
default: 1,
}
})
</script>
<template>
<NGrid :cols="cols">
<slot />
</NGrid>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,19 @@
<script setup>
import { NGi } from 'naive-ui'
const props = defineProps({
span: {
type: Number,
default: 1,
}
})
</script>
<template>
<NGi :span="span">
<slot />
</NGi>
</template>
<style scoped>
</style>

View File

@@ -6,6 +6,10 @@ const props = defineProps({
type: String,
default: ''
},
headerIncludeBody: {
type: Boolean,
default: false
},
feedback: {
type: String,
default: ''
@@ -25,6 +29,7 @@ const props = defineProps({
})
const hasHeader = computed(() => props.header.trim().length > 0)
const hasHeaderInOutside = computed(() => hasHeader.value && !props.headerIncludeBody)
const hasFeedback = computed(() => props.feedback.trim().length > 0)
const hasMinH = computed(() => props.minH.trim().length > 0 || Number.isInteger(props.minH))
const hasMaxH = computed(() => props.maxH.trim().length > 0 || Number.isInteger(props.maxH))
@@ -55,8 +60,11 @@ watch(() => [props.minH, props.maxH], ([minH, maxH]) => {
</script>
<template>
<NFormItem :show-label="hasHeader" :label="header" :show-feedback="hasFeedback" :feedback="feedback">
<NFormItem :show-label="hasHeaderInOutside" :label="header" :show-feedback="hasFeedback" :feedback="feedback">
<NCard :class="noPadding ? 'no-padding h-full' : ''">
<template v-if="!hasHeaderInOutside" #header>
{{ header }}
</template>
<NScrollbar :style="styles">
<slot />
</NScrollbar>

View File

@@ -27,6 +27,8 @@ const form = reactive({
birth_date: null,
admitted_at: null,
patient_kind: 'plan',
manual_status: 'current',
outcome_at: null,
diagnosis_code: '',
diagnosis_name: '',
})
@@ -56,6 +58,12 @@ const patientKindOptions = [
{label: 'Экстренно', value: 'emergency'},
]
const manualStatusOptions = [
{label: 'Состоит', value: 'current'},
{label: 'Выписан', value: 'discharged'},
{label: 'Умер', value: 'deceased'},
]
const normalizeSpaces = (value) => value.replace(/\s+/g, ' ').trim()
const pad = (value) => String(value).padStart(2, '0')
const formatLocalDate = (timestamp) => {
@@ -76,6 +84,11 @@ const parseDateToTimestamp = (value) => {
}
const parseDateTimeToTimestamp = (value) => {
if (!value) return null
const nativeDate = new Date(value)
if (!Number.isNaN(nativeDate.getTime())) {
return nativeDate.getTime()
}
const [datePart, timePart] = String(value).split(' ')
const dateTimestamp = parseDateToTimestamp(datePart)
if (!dateTimestamp) return null
@@ -90,6 +103,8 @@ const hydrateForm = () => {
form.birth_date = parseDateToTimestamp(props.patient?.birth_date)
form.admitted_at = parseDateTimeToTimestamp(props.patient?.admitted_at)
form.patient_kind = props.patient?.patient_kind ?? 'plan'
form.manual_status = props.patient?.outcome_type ?? 'current'
form.outcome_at = parseDateTimeToTimestamp(props.patient?.outcome_date)
form.diagnosis_code = props.patient?.mkb?.ds ?? ''
form.diagnosis_name = props.patient?.mkb?.name ?? ''
@@ -179,11 +194,21 @@ const submit = async () => {
birth_date: formatLocalDate(form.birth_date),
admitted_at: form.admitted_at ? formatLocalDateTime(form.admitted_at) : null,
patient_kind: form.patient_kind,
manual_status: form.manual_status,
outcome_at: form.manual_status === 'current'
? null
: (form.outcome_at ? formatLocalDateTime(form.outcome_at) : null),
diagnosis_code: form.diagnosis_code ? normalizeSpaces(form.diagnosis_code).toUpperCase() : null,
diagnosis_name: form.diagnosis_name ? normalizeSpaces(form.diagnosis_name) : null,
},
{
reloadStatuses: [props.sourceStatus, `special-${form.patient_kind}`],
reloadStatuses: [
props.sourceStatus,
`special-${form.patient_kind}`,
'special-outcome-discharged',
'special-outcome-deceased',
'special-outcome-transferred',
],
}
)
@@ -204,12 +229,18 @@ const submit = async () => {
<NFormItem label="Тип пациента" path="patient_kind">
<NSelect v-model:value="form.patient_kind" :options="patientKindOptions" />
</NFormItem>
<NFormItem label="Статус">
<NSelect v-model:value="form.manual_status" :options="manualStatusOptions" />
</NFormItem>
<NFormItem label="Дата рождения" path="birth_date">
<NDatePicker v-model:value="form.birth_date" type="date" class="w-full" />
</NFormItem>
<NFormItem label="Дата и время поступления">
<NDatePicker v-model:value="form.admitted_at" type="datetime" class="w-full" clearable />
</NFormItem>
<NFormItem v-if="form.manual_status !== 'current'" label="Дата и время исхода">
<NDatePicker v-model:value="form.outcome_at" type="datetime" class="w-full" clearable />
</NFormItem>
<NFormItem label="Диагноз (МКБ)">
<NSelect
v-model:value="form.diagnosis_code"

View File

@@ -134,6 +134,10 @@ const submit = async () => {
try {
await reportStore.createManualPatient({
departmentId: reportStore.reportInfo.department.department_id,
report_id: reportStore.reportInfo?.report?.report_id ?? null,
startAt: reportStore.reportInfo?.dates?.startAt,
endAt: reportStore.reportInfo?.dates?.endAt,
user_id: reportStore.reportInfo?.report?.userId ?? null,
full_name: normalizeSpaces(form.full_name),
birth_date: formatLocalDate(form.birth_date),
patient_kind: props.patientKind,

View File

@@ -25,23 +25,44 @@ const onSubmit = () => {
// reportStore.sendReportForm()
}
const onPublish = () => {
reportStore.reportFormRef?.validate((errors) => {
if (!errors) reportStore.sendReportForm({ status: 'submitted' })
else window.$message.error('Ошибка отправки отчета')
})
}
</script>
<template>
<NFlex vertical class="max-w-6xl mx-auto mt-6 mb-4 w-full">
<NAlert type="warning" v-if="reportStore.reportInfo.report?.message">
{{ reportStore.reportInfo.report.message }}
</NAlert>
<NFlex v-if="reportStore.reportInfo.report?.message || reportStore.reportInfo.report?.statusMessage" :size="12">
<NAlert class="flex-1" type="warning" v-if="reportStore.reportInfo.report?.message">
{{ reportStore.reportInfo.report.message }}
</NAlert>
<NAlert class="flex-1" type="info" v-if="reportStore.reportInfo.report?.statusMessage">
{{ reportStore.reportInfo.report.statusMessage }}
</NAlert>
</NFlex>
<ReportHeader :mode="mode" />
<ReportFormInput />
<ReportSection label="Планово" />
<NButton v-if="reportStore.reportInfo?.report?.isActiveSendButton" secondary size="large" @click="onSubmit">
Сохранить отчет
</NButton>
<NFlex v-if="reportStore.reportInfo?.report?.isActiveSendButton" :size="12">
<NButton secondary size="large" @click="onSubmit">
Сохранить отчет
</NButton>
<NButton
v-if="reportStore.reportInfo?.report?.canPublish || !reportStore.reportInfo?.report?.report_id"
type="primary"
size="large"
@click="onPublish"
>
Опубликовать
</NButton>
</NFlex>
</NFlex>
</template>

View File

@@ -217,14 +217,6 @@ watch(
</NAlert>
</NFlex>
</NCollapseItem>
<NCollapseItem name="4">
<template #header>
<ReportSectionHeader title="Находятся в реанимации" status="special-reanimation" />
</template>
<ReportSectionItem status="special-reanimation"
:enabled="activeRootTab === 'special' && reportStore.openedCollapsible.includes('4')"
/>
</NCollapseItem>
<NCollapseItem name="5">
<template #header>
<ReportSectionHeader title="Выбывшие" status="special-outcome" />

View File

@@ -18,7 +18,7 @@ import {
import {useReportStore} from "../../../Stores/report.js";
import {computed, h, ref, watch} from "vue";
import {storeToRefs} from "pinia";
import {TbEye, TbExternalLink, TbPencil} from "vue-icons-plus/tb";
import {TbEye, TbExternalLink, TbPencil, TbTrash} from "vue-icons-plus/tb";
import MoveModalComment from "./MoveModalComment.vue";
import OperationInfoModal from "./OperationInfoModal.vue";
import ManualPatientOutcomeModal from "./ManualPatientOutcomeModal.vue";
@@ -90,7 +90,11 @@ const activePatient = ref(null)
const hasDisabledEdit = computed(() => {
return !Boolean(reportStore.reportInfo?.report?.isActiveSendButton)
})
const canEditSpecial = computed(() => isSpecialStatus.value && !hasDisabledEdit.value)
const canEditSpecial = computed(() => (
isSpecialStatus.value
&& !hasDisabledEdit.value
&& baseStatus.value !== 'observation'
))
const statusState = computed(() => statusStates.value[props.status] ?? {
page: 1,
perPage: 20,
@@ -214,7 +218,7 @@ const columns = computed(() => {
}
},
[
'Снять с наблюдения'
h(NIcon, {size: '16'}, h(TbTrash))
]
)
}

View File

@@ -0,0 +1,29 @@
<script setup>
import AppLayout from "../Layouts/AppLayout.vue"
import AppPanel from "../Components/AppPanel.vue";
import AppGridItem from "../Components/AppGridItem.vue";
import AppGrid from "../Components/AppGrid.vue";
import {NGi} from 'naive-ui'
</script>
<template>
<AppLayout>
<AppGrid cols="5">
<NGi>
<AppPanel header="Текст" header-include-body>
panel
</AppPanel>
</NGi>
<NGi>
<AppPanel header="Текст" header-include-body>
panel
</AppPanel>
</NGi>
</AppGrid>
</AppLayout>
</template>
<style scoped>
</style>

View File

@@ -14,7 +14,6 @@ const patientStatuses = [
'special-plan',
'special-emergency',
'special-observation',
'special-reanimation',
'special-outcome-discharged',
'special-outcome-deceased',
'special-outcome-transferred',
@@ -260,6 +259,17 @@ export const useReportStore = defineStore('reportStore', () => {
})
})
const getSpecialStatusByPatient = (patient) => {
if (!patient) return null
if (patient.is_current) {
return patient.patient_kind ? `special-${patient.patient_kind}` : null
}
if (!patient.outcome_type) return null
return `special-outcome-${patient.outcome_type}`
}
const getDataOnReportDate = async () => {
await reloadReportPage()
}
@@ -276,19 +286,30 @@ export const useReportStore = defineStore('reportStore', () => {
userId: reportInfo.value.report.userId,
departmentId: reportInfo.value.department.department_id,
reportId: reportInfo.value.report.report_id,
status: reportInfo.value?.report?.status ?? 'draft',
...assignForm
}
router.post('/report', form, {
onSuccess: () => {
window.$message.success('Отчет сохранен')
window.$message.success(form.status === 'submitted' ? 'Отчет опубликован' : 'Черновик сохранен')
}
})
}
const createManualPatient = async (payload) => {
await axios.post('/api/report/manual-patients', payload)
await reloadReportPage()
const response = await axios.post('/api/report/manual-patients', payload)
const reportId = response.data?.report_id
if (reportId) {
reportInfo.value = {
...reportInfo.value,
report: {
...(reportInfo.value?.report ?? {}),
report_id: reportId,
}
}
}
const status = `special-${payload.patient_kind}`
@@ -299,9 +320,13 @@ export const useReportStore = defineStore('reportStore', () => {
}
const setManualPatientOutcome = async (departmentPatientId, payload) => {
await axios.post(`/api/report/manual-patients/${departmentPatientId}/outcome`, payload)
await reloadReportPage()
await loadAllStatusCounts(true)
const response = await axios.post(`/api/report/manual-patients/${departmentPatientId}/outcome`, payload)
const targetStatus = getSpecialStatusByPatient(response.data)
await Promise.all([
...(targetStatus ? [loadPatientsByStatus(targetStatus, { resetPage: true })] : []),
loadAllStatusCounts(true),
])
}
const updateManualPatient = async (departmentPatientId, payload, options = {}) => {
@@ -313,8 +338,6 @@ export const useReportStore = defineStore('reportStore', () => {
const reloadStatuses = Array.from(new Set((options.reloadStatuses ?? []).filter(Boolean)))
await reloadReportPage()
await Promise.all([
...reloadStatuses.map((status) => loadPatientsByStatus(status, { resetPage: true })),
loadAllStatusCounts(true),