This commit is contained in:
brusnitsyn
2026-02-20 17:28:16 +09:00
parent 94e374c32b
commit 52a80ccd3b
41 changed files with 2555 additions and 206 deletions

View File

@@ -0,0 +1,173 @@
<script setup>
import {
NModal,
NList,
NListItem,
NThing,
NAvatar,
NIcon,
NText,
NDivider,
NFlex,
NButton,
NScrollbar,
NEmpty,
NDataTable,
NBadge,
NForm,
NDrawerContent,
NInput,
NSpin,
NDrawer
} from 'naive-ui'
import {useReportStore} from "../../../Stores/report.js";
import {TbAlertCircle, TbCheck, TbEye, TbGripVertical, TbPencil, TbTrashX, TbX} from 'vue-icons-plus/tb'
import {computed, h, ref, watch} from "vue";
const props = defineProps({
departmentId: {
required: true
},
startAt: {
required: true
},
endAt: {
required: true
}
})
const reportStore = useReportStore()
const open = defineModel('open')
const loading = ref(true)
const baseColumns = reportStore.getColumnsByKey(['num', 'fullname', 'age', 'birth_date', 'mkb.ds'])
const currentPatient = ref(null)
const showMoveDrawer = ref(false)
const columns = computed(() => {
// if (!isFillableMode.value) return baseColumns
const newColumns = []
const expandColumn = {
title: '',
width: '30',
render: (rowData) => {
return h(
NIcon,
{
onClick: () => {
currentPatient.value = rowData
showMoveDrawer.value = true
}
},
{
default: h(TbEye)
}
)
}
}
const fillableColumn = {
title: '',
key: 'fillable',
width: '20',
render: (row) => h(
NBadge,
{
dot: true,
color: (row.comment && row.comment.trim()) ? '#7fe7c4' : '#e88080'
}
)
}
newColumns.push(expandColumn)
newColumns.push(fillableColumn)
newColumns.push(...baseColumns)
return newColumns
})
const observablePatients = ref([])
const fetchUnwantedEvents = () => {
loading.value = true
const data = {
status: 'observation',
startAt: props.startAt,
endAt: props.endAt,
departmentId: props.departmentId
}
axios.post('/api/mis/patients', data).then((res) => {
observablePatients.value = reportStore.addRowNumbers(res.data)
}).finally(() => {
loading.value = false
})
}
watch(() => [props.departmentId, props.endAt, props.startAt], () => {
if (props.departmentId && props.endAt && props.startAt) {
fetchUnwantedEvents()
}
}, {
immediate: true,
deep: true
})
</script>
<template>
<NModal v-model:show="open"
title="Пациенты на контроле"
preset="card"
:mask-closable="false"
:close-on-esc="false"
class="max-w-4xl overflow-clip h-[calc(100vh-220px)]"
id="modal-observation-patients"
>
<template v-if="loading">
<div class="flex items-center justify-center h-full">
<NSpin />
</div>
</template>
<template v-else-if="observablePatients.length">
<NDataTable :columns="columns"
ref="tableRef"
:data="observablePatients"
size="small"
:loading="loading"
max-height="200"
min-height="200"
:row-key="(row, index) => row.id"
class="text-sm!">
</NDataTable>
</template>
<template v-else>
<div class="h-full flex items-center justify-center">
<NEmpty description="Пациентов на контроле не найдено!" />
</div>
</template>
<NDrawer
v-model:show="showMoveDrawer"
placement="bottom"
:min-height="400"
:max-height="600"
:default-height="400"
resizable
:trap-focus="false"
:block-scroll="false"
to="#modal-observation-patients"
>
<NDrawerContent title="Причина постановки на контроль" closable>
<NInput type="textarea" readonly :rows="8" v-model:value="currentPatient.comment" />
</NDrawerContent>
</NDrawer>
</NModal>
</template>
<style scoped>
:deep(.n-data-table-th),
:deep(.n-data-table-td) {
white-space: nowrap !important;
font-size: var(--n-font-size);
}
</style>

View File

@@ -0,0 +1,115 @@
<script setup>
import {
NModal, NList, NListItem, NThing, NAvatar, NIcon,
NText, NDivider, NFlex, NButton, NScrollbar, NEmpty, NSpin
} from 'naive-ui'
import {useReportStore} from "../../../Stores/report.js";
import { TbAlertCircle, TbPencil, TbTrashX } from 'vue-icons-plus/tb'
import {ref, watch} from "vue";
const props = defineProps({
departmentId: {
required: true
},
startAt: {
required: true
},
endAt: {
required: true
}
})
const open = defineModel('open')
const loading = ref(true)
const unwantedEvents = ref([])
const fetchUnwantedEvents = () => {
loading.value = true
axios.get('/api/statistics/reports/unwanted-events', {
params: {
departmentId: props.departmentId,
startAt: props.startAt,
endAt: props.endAt
}
})
.then(res => {
unwantedEvents.value = res.data
})
.finally(() => {
loading.value = false
})
}
watch(() => [props.departmentId, props.endAt, props.startAt], () => {
if (props.departmentId && props.endAt && props.startAt) {
fetchUnwantedEvents()
}
}, {
immediate: true,
deep: true
})
</script>
<template>
<NModal v-model:show="open"
title="Нежелательные события"
preset="card"
:mask-closable="false"
:close-on-esc="false"
class="max-w-4xl overflow-clip h-[calc(100vh-220px)]"
>
<template v-if="loading">
<div class="flex items-center justify-center h-full">
<NSpin />
</div>
</template>
<template v-else-if="unwantedEvents.length">
<NScrollbar class="max-h-[calc(100vh-282px)] pr-3">
<NList>
<NListItem v-for="event in unwantedEvents">
<NThing>
<template #avatar>
<NAvatar>
<NIcon>
<TbAlertCircle class="text-red-400" />
</NIcon>
</NAvatar>
</template>
<template #header>
{{ event.title }}
</template>
<template #description>
<NText depth="3">
{{ event.created_at }}
</NText>
</template>
<NText>
{{ event.comment }}
</NText>
</NThing>
</NListItem>
</NList>
</NScrollbar>
</template>
<template v-else>
<div class="h-full flex items-center justify-center">
<NEmpty description="Нежелательные события не найдены!" />
</div>
</template>
<!-- <template #action>-->
<!-- <NFlex id="modal-action" align="center" justify="space-between">-->
<!-- <NButton type="primary" secondary @click="onCreateEvent()">-->
<!-- <template #icon>-->
<!-- <TbCirclePlus />-->
<!-- </template>-->
<!-- Создать событие-->
<!-- </NButton>-->
<!-- </NFlex>-->
<!-- </template>-->
</NModal>
</template>
<style scoped>
</style>

View File

@@ -1,9 +1,12 @@
<script setup>
import {NDataTable, NFlex, NText, NDatePicker} from 'naive-ui'
import {NDataTable, NFlex, NText, NDatePicker, NBadge, NIcon, NPopover, NTag, NSpace} from 'naive-ui'
import AppLayout from "../../Layouts/AppLayout.vue";
import {h, ref} from "vue";
import DatePickerQuery from "../../Components/DatePickerQuery.vue";
import {Link, usePage} from "@inertiajs/vue3";
import {TbAlertCircle, TbEye} from "vue-icons-plus/tb";
import ModalUnwantedEvents from "./Components/ModalUnwantedEvents.vue";
import ModalObservablePatients from "./Components/ModalObservablePatients.vue";
const props = defineProps({
data: {
@@ -37,7 +40,7 @@ const columns = ref([
}
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.department)
return row.department
}
// Получаем текущие query параметры
@@ -46,8 +49,10 @@ const columns = ref([
const searchParams = currentUrl.searchParams
// Берем startAt и endAt из текущего URL
const startAt = searchParams.get('startAt')
const endAt = searchParams.get('endAt')
const propsStartAt = Array.isArray(props.date) ? props.date[0] : props.date
const propsEndAt = Array.isArray(props.date) ? props.date[1] : props.date
const startAt = searchParams.get('startAt') ?? propsStartAt
const endAt = searchParams.get('endAt') ?? propsEndAt
const linkData = {}
@@ -56,11 +61,53 @@ const columns = ref([
if (endAt)
linkData.endAt = endAt
return h(Link, {
href: `/report`,
data: linkData,
class: 'underline decoration-dashed'
}, row.department)
return h(NFlex, {align: 'center', justify: 'start'}, [
h(NBadge, {
dot: true,
type: row.isReportToday ? 'success' : 'error'
}),
h(Link, {
href: `/report`,
data: linkData,
class: 'underline decoration-dashed'
}, row.department),
h(NSpace, {align: 'center', size: 'small'}, [
h(NPopover, {
trigger: 'hover',
}, {
trigger: h(NTag, {
round: true,
size: 'small',
bordered: false,
class: 'cursor-pointer!',
onClick: () => onShowUnwantedEventsModal(row.department_id)
}, {
icon: h(NIcon, { }, {
default: () => h(TbAlertCircle)
}),
default: row.countUnwanted
}),
default: 'Нежелательные события'
}),
h(NPopover, {
trigger: 'hover',
}, {
trigger: h(NTag, {
round: true,
size: 'small',
bordered: false,
class: 'cursor-pointer!',
onClick: () => onShowObservablePatientsModal(row.department_id)
}, {
icon: h(NIcon, { }, {
default: () => h(TbEye)
}),
default: row.countObservable
}),
default: 'Пациенты на контроле'
}),
])
])
}
},
{
@@ -69,13 +116,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.beds)
} else {
return h(NText, { }, row.beds)
}
}
},
{
title: 'Поступило',
@@ -88,13 +128,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.recipients.all)
} else {
return h(NText, { }, row.recipients.all)
}
}
},
{
title: 'План',
@@ -102,13 +135,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.recipients.plan)
} else {
return h(NText, { }, row.recipients.plan)
}
}
},
{
title: 'Экстр',
@@ -116,13 +142,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.recipients.emergency)
} else {
return h(NText, { }, row.recipients.emergency)
}
}
},
{
title: 'Перевод',
@@ -130,13 +149,6 @@ const columns = ref([
width: 84,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.recipients.transferred)
} else {
return h(NText, { }, row.recipients.transferred)
}
}
},
]
},
@@ -146,13 +158,6 @@ const columns = ref([
width: 84,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.outcome)
} else {
return h(NText, { }, row.outcome)
}
}
},
{
title: 'Состоит',
@@ -160,13 +165,13 @@ const columns = ref([
width: 84,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.consist)
} else {
return h(NText, { }, row.consist)
}
}
},
{
title: 'Ср. койко-день',
key: 'averageBedDays',
width: 44,
titleAlign: 'center',
align: 'center',
},
{
title: '% загруженности',
@@ -174,13 +179,6 @@ const columns = ref([
width: 84,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.percentLoadedBeds)
} else {
return h(NText, { }, row.percentLoadedBeds)
}
}
},
{
title: 'Операции',
@@ -193,13 +191,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.surgical.emergency)
} else {
return h(NText, { }, row.surgical.emergency)
}
}
},
{
title: 'П',
@@ -207,13 +198,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.surgical.plan)
} else {
return h(NText, { }, row.surgical.plan)
}
}
},
]
},
@@ -223,24 +207,41 @@ const columns = ref([
width: 84,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.deceased)
} else {
return h(NText, { }, row.deceased)
}
}
},
{
title: 'Мед. персонал',
key: 'countStaff',
width: 84,
titleAlign: 'center',
align: 'center',
},
])
const currentDepartmentId = ref(null)
const showUnwantedEventsModal = ref(false)
const showObservablePatientsModal = ref(false)
const onShowUnwantedEventsModal = (departmentId) => {
currentDepartmentId.value = departmentId
showUnwantedEventsModal.value = true
}
const onShowObservablePatientsModal = (departmentId) => {
currentDepartmentId.value = departmentId
showObservablePatientsModal.value = true
}
const rowProps = (row) => {
if (row.isGroupHeader) return {
style: `--n-merged-td-color: var(--n-merged-th-color)`
}
if (row.isTotalRow) return {
style: `--n-merged-td-color: var(--n-merged-th-color); --n-text-color: var(--n-th-icon-color-active);`
style: `--n-merged-td-color: var(--n-merged-th-color);`
}
}
const rowClassName = (row) => {
if (row.isTotalRow)
return `total-row`
}
</script>
<template>
@@ -256,9 +257,12 @@ const rowProps = (row) => {
min-height="calc(100vh - 48px - 70px)"
max-height="calc(100vh - 48px - 70px)"
:row-props="rowProps"
:row-class-name="rowClassName"
>
</NDataTable>
<ModalUnwantedEvents v-model:open="showUnwantedEventsModal" :start-at="date[0]" :end-at="date[1]" :department-id="currentDepartmentId" />
<ModalObservablePatients v-model:open="showObservablePatientsModal" :start-at="date[0]" :end-at="date[1]" :department-id="currentDepartmentId" />
</AppLayout>
</template>
@@ -267,4 +271,9 @@ const rowProps = (row) => {
:deep(.n-data-table-td) {
font-size: var(--n-font-size);
}
:deep(.total-row td) {
--n-td-text-color: var(--n-th-icon-color-active);
font-weight: 500;
}
</style>