Files
onboard/resources/js/Pages/Statistic/Index.vue
2026-03-25 17:37:32 +09:00

378 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 {
NDataTable,
NFlex,
NText,
NDivider,
NBadge,
NIcon,
NPopover,
NTag,
NSpace,
NButton,
NNumberAnimation
} from 'naive-ui'
import AppLayout from "../../Layouts/AppLayout.vue";
import {h, ref} from "vue";
import DatePickerQuery from "../../Components/DatePickerQuery.vue";
import {Link, router, usePage} from "@inertiajs/vue3";
import {TbAlertCircle, TbEye} from "vue-icons-plus/tb";
import {PiMicrosoftExcelLogo} from "vue-icons-plus/pi";
import ModalUnwantedEvents from "./Components/ModalUnwantedEvents.vue";
import ModalObservablePatients from "./Components/ModalObservablePatients.vue";
import StatisticRecipientPlanOfYear from "../../Layouts/Components/Statistic/StatisticRecipientPlanOfYear.vue";
import {percentType} from "../../Utils/numbers.js";
const props = defineProps({
data: {
type: Object,
default: []
},
isHeadOrAdmin: {
type: Boolean
},
date: {
type: [Number, Array]
},
isOneDay: {
type: Boolean
},
recipientPlanOfYear: {
type: Object,
default: {}
}
})
const columns = ref([
{
title: 'Отделение',
key: 'department',
width: 200,
titleAlign: 'center',
cellProps: (row) => ({
style: '--n-merged-td-color: var(--n-merged-th-color); --n-merged-td-color-striped: var(--n-merged-th-color); --n-td-text-color: var(--color-zinc-400); --n-th-text-color: var(--color-zinc-400);'
}),
colSpan: (row) => row.colspan,
render(row) {
if (row.isGroupHeader) {
return h(NFlex, {
align: "center",
justify: "center"
}, h(NText, { style: 'font-weight: 600; color: var(--color-zinc-400)' }, row.groupName))
}
if (row.isTotalRow) {
return row.department
}
// Получаем текущие query параметры
const { url } = usePage()
const currentUrl = new URL(url, window.location.origin)
const searchParams = currentUrl.searchParams
// Берем startAt и endAt из текущего URL
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 = {}
if (startAt)
linkData.startAt = startAt
if (endAt)
linkData.endAt = endAt
linkData.departmentId = row.department_id
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: 'Пациенты на контроле'
}),
])
])
}
},
{
title: 'Кол-во коек',
key: 'beds',
width: 60,
titleAlign: 'center',
align: 'center',
},
{
title: 'Поступило',
key: 'recipients',
titleAlign: 'center',
children: [
{
title: 'Всего',
key: 'recipients.all',
width: 60,
titleAlign: 'center',
align: 'center',
},
{
title: 'План',
key: 'recipients.plan',
width: 60,
titleAlign: 'center',
align: 'center',
},
{
title: 'Экстр',
key: 'recipients.emergency',
width: 60,
titleAlign: 'center',
align: 'center',
},
{
title: 'Перевод',
key: 'recipients.transferred',
width: 84,
titleAlign: 'center',
align: 'center',
},
]
},
{
title: 'Выбыло',
key: 'outcome',
width: 84,
titleAlign: 'center',
align: 'center',
render(row) {
console.log(row)
return h(
'div',
{
class: 'relative'
},
[
h('div', {}, row.outcome),
!row.isTotalRow ? h('div', {
class: 'absolute -right-1.5 bottom-2.5 text-xs',
}, [
h(NTag, {size: 'tiny', round: true, bordered: false, type: percentType(row.percentPlanOfYear)}, `${row.percentPlanOfYear}%`)
]) : null
]
)
}
},
{
title: 'Состоит',
key: 'consist',
width: 84,
titleAlign: 'center',
align: 'center',
},
{
title: 'Ср. койко-день',
key: 'averageBedDays',
width: 44,
titleAlign: 'center',
align: 'center',
},
{
title: 'Пред. опер. койко-день',
key: 'preoperativeDays',
width: 82,
titleAlign: 'center',
align: 'center',
},
{
title: '% загруженности',
key: 'percentLoadedBeds',
width: 78,
titleAlign: 'center',
align: 'center',
},
{
title: '% летальности',
key: 'lethality',
width: 66,
titleAlign: 'center',
align: 'center',
},
{
title: 'Операции',
key: 'surgical',
titleAlign: 'center',
children: [
{
title: 'Э',
key: 'surgical.emergency',
width: 44,
titleAlign: 'center',
align: 'center',
},
{
title: 'П',
key: 'surgical.plan',
width: 44,
titleAlign: 'center',
align: 'center',
},
]
},
{
title: 'Умерло',
key: 'deceased',
width: 48,
titleAlign: 'center',
align: 'center',
},
{
title: 'Мед. персонал',
key: 'countStaff',
width: 54,
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);`
}
}
const rowClassName = (row) => {
if (row.isTotalRow)
return `total-row`
}
const downloadReport = async () => {
try {
const response = await fetch(`/statistic/report?startAt=${props.date[0]}&endAt=${props.date[1]}`, {
method: 'GET',
headers: {
'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
},
})
if (!response.ok) {
throw new Error('Ошибка загрузки файла')
}
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'Статистика по отделениям.xlsx'
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (error) {
console.error('Ошибка скачивания:', error)
}
}
</script>
<template>
<AppLayout>
<template #headerExtra>
<NSpace align="center">
<DatePickerQuery :is-head-or-admin="isHeadOrAdmin" :is-show-current-date-switch="true" :date="date" :is-one-day="isOneDay" />
<NDivider vertical />
<NButton type="info" secondary @click="downloadReport">
<template #icon>
<PiMicrosoftExcelLogo />
</template>
Сохранить в Excel
</NButton>
</NSpace>
</template>
<template #headerSuffix>
<StatisticRecipientPlanOfYear :plan="recipientPlanOfYear.plan" :progress="recipientPlanOfYear.progress" />
</template>
<NDataTable :columns="columns"
:data="data"
size="small"
:single-line="false"
striped
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>
<style scoped>
:deep(.n-data-table-th),
:deep(.n-data-table-td) {
font-size: var(--n-font-size);
}
:deep(.n-data-table-th) {
color: var(--color-zinc-400);
}
:deep(.total-row td) {
--n-td-text-color: var(--n-th-icon-color-active);
font-weight: 500;
}
</style>