* блокировка изменения отчета для врача

* вывод данных из отчетов для ролей адм и зав
* поправил ширину стобцов ввода
* добавил календарь на страницу статистики
* переделал календарь у заведующего на странице отчета
* добавил и привязал метрики в статистику
This commit is contained in:
brusnitsyn
2026-02-03 17:03:37 +09:00
parent 2805e5e4bc
commit 9ee33bc517
20 changed files with 889 additions and 159 deletions

View File

@@ -0,0 +1,117 @@
<script setup>
import {NDatePicker, NIcon, } from 'naive-ui'
import {computed, onMounted, ref, watch} from "vue";
import {router} from "@inertiajs/vue3";
import {TbCalendar} from 'vue-icons-plus/tb'
import {formatRussianDate, formatRussianDateRange} from "../Utils/dateFormatter.js";
const props = defineProps({
// Пользователь с ролью админ или зав
isHeadOrAdmin: {
type: Boolean,
default: false
},
date: {
type: [Number, Array]
},
isOneDay: {
type: Boolean
}
})
const isUseDateRange = computed(() => props.isHeadOrAdmin)
const datePicker = ref()
const showCalendar = ref(false)
const dateType = computed(() => {
if (isUseDateRange.value) return 'daterange'
return 'date'
})
const queryDate = ref([null, null])
const modelValue = ref(props.date)
const setQueryDate = () => {
router.reload({
data: {
startAt: queryDate.value[0],
endAt: queryDate.value[1]
}
})
}
// Форматированное значение для отображения
const formattedValue = computed(() => {
const value = modelValue.value
if (props.isHeadOrAdmin) {
if (props.isOneDay) {
const dateToFormat = Array.isArray(value) ? value[1] : value
return formatRussianDate(dateToFormat)
} else if (Array.isArray(value) && value.length >= 2 && value[0] && value[1]) { // Для админа - диапазон дат
return formatRussianDateRange(value)
}
// Если что-то пошло не так, форматируем как одиночную дату
if (value) {
const dateToFormat = Array.isArray(value) ? value[0] : value
return formatRussianDate(dateToFormat)
}
return ''
} else {
// Для врача - одиночная дата
let dateToFormat
if (Array.isArray(value)) {
dateToFormat = value[1] || value[0]
} else {
dateToFormat = value
}
// Если выбрана сегодняшняя дата - показываем текущее время
if (dateToFormat) {
return formatRussianDate(dateToFormat)
}
return ''
}
})
watch(() => modelValue.value, (newVal) => {
if (isUseDateRange.value) {
queryDate.value = newVal
} else {
queryDate.value = [newVal, newVal]
}
setQueryDate()
})
</script>
<template>
<div class="relative inline-flex items-center">
<div v-if="formattedValue" class="font-medium leading-3 cursor-pointer" @click="showCalendar = true">
{{ formattedValue }}
</div>
<NDatePicker v-model:value="modelValue"
v-model:show="showCalendar"
ref="datePicker"
class="opacity-0 absolute! inset-x-0 bottom-full -translate-y-1/2"
placement="top-start"
input-readonly
bind-calendar-months
update-value-on-close
:type="dateType" />
<div class="cursor-pointer p-2 flex items-center justify-center" @click="showCalendar = true">
<NIcon size="20">
<TbCalendar />
</NIcon>
</div>
</div>
</template>
<style scoped>
:deep(.n-input) {
position: absolute;
pointer-events: none;
}
</style>

View File

@@ -1,9 +1,17 @@
<script setup>
import {NDatePicker} from 'naive-ui'
import {NDatePicker, NIcon} from 'naive-ui'
import {storeToRefs} from "pinia";
import {useReportStore} from "../Stores/report.js";
import {useAuthStore} from "../Stores/auth.js";
import {computed, ref, onMounted, onUnmounted} from "vue";
import {TbCalendar} from "vue-icons-plus/tb";
import {formatRussianDate, formatRussianDateRange} from "../Utils/dateFormatter.js";
const props = defineProps({
isOneDay: {
type: Boolean
}
})
const reportStore = useReportStore()
const authStore = useAuthStore()
@@ -12,22 +20,6 @@ const {timestampCurrentRange} = storeToRefs(reportStore)
// Текущее время для обновления
const currentTime = ref(Date.now())
// Обновляем время каждую секунду
let intervalId = null
onMounted(() => {
if (authStore.isDoctor) {
intervalId = setInterval(() => {
currentTime.value = Date.now()
}, 1000)
}
})
onUnmounted(() => {
if (intervalId) {
clearInterval(intervalId)
}
})
// Проверяем, является ли дата сегодняшней
const isToday = (timestamp) => {
const date = new Date(timestamp)
@@ -121,11 +113,46 @@ const modelComputed = computed({
const userId = params.get('userId')
reportStore.reportInfo.userId = userId
reportStore.getDataOnReportDate(reportStore.timestampCurrentRange)
}
})
const formattedValue = computed(() => {
const value = reportStore.timestampCurrentRange
if (authStore.isHeadOfDepartment || authStore.isAdmin) {
if (props.isOneDay) {
const dateToFormat = Array.isArray(value) ? value[1] : value
return formatRussianDate(dateToFormat)
} else if (Array.isArray(value) && value.length >= 2 && value[0] && value[1]) { // Для админа - диапазон дат
return formatRussianDateRange(value)
}
// Если что-то пошло не так, форматируем как одиночную дату
if (value) {
const dateToFormat = Array.isArray(value) ? value[0] : value
return formatRussianDate(dateToFormat)
}
return ''
} else {
// Для врача - одиночная дата
let dateToFormat
if (Array.isArray(value)) {
dateToFormat = value[1] || value[0]
} else {
dateToFormat = value
}
// Если выбрана сегодняшняя дата - показываем текущее время
if (dateToFormat) {
return formatRussianDate(dateToFormat)
}
return ''
}
})
const classComputed = computed(() => {
const baseClasses = []
@@ -138,28 +165,48 @@ const classComputed = computed(() => {
return baseClasses
})
const showCalendar = ref(false)
</script>
<template>
<NDatePicker
:theme-overrides="themeOverride"
v-model:value="modelComputed"
:class="classComputed"
:format="formatComputed"
:type="typeComputed"
placement="top-end"
input-readonly
bind-calendar-months
update-value-on-close
/>
<div class="relative inline-flex items-center">
<div v-if="formattedValue" class="text-lg font-medium leading-3 cursor-pointer" @click="showCalendar = true">
{{ formattedValue }}
</div>
<NDatePicker v-model:value="modelComputed"
v-model:show="showCalendar"
ref="datePicker"
class="opacity-0 absolute! inset-x-0 bottom-full -translate-y-1/2"
placement="top-start"
input-readonly
bind-calendar-months
update-value-on-close
:type="typeComputed" />
<div class="cursor-pointer p-2 flex items-center justify-center" @click="showCalendar = true">
<NIcon size="20">
<TbCalendar />
</NIcon>
</div>
</div>
<!-- <NDatePicker-->
<!-- :theme-overrides="themeOverride"-->
<!-- v-model:value="modelComputed"-->
<!-- :class="classComputed"-->
<!-- :format="formatComputed"-->
<!-- :type="typeComputed"-->
<!-- placement="top-end"-->
<!-- input-readonly-->
<!-- bind-calendar-months-->
<!-- update-value-on-close-->
<!-- />-->
</template>
<style scoped>
:deep(.n-input__suffix) {
margin-left: 12px;
}
:deep(.n-input),
:deep(.n-input__input-el) {
cursor: pointer !important;
:deep(.n-input) {
position: absolute;
pointer-events: none;
}
</style>

View File

@@ -22,7 +22,11 @@ const themeOverrides = {
<NLayout position="absolute">
<NLayoutHeader style="height: 48px;" bordered>
<AppHeader />
<AppHeader>
<template #headerExtra>
<slot name="headerExtra" />
</template>
</AppHeader>
</NLayoutHeader>
<NLayout has-sider position="absolute" class="top-12!" content-class="relative" :native-scrollbar="false">

View File

@@ -4,13 +4,21 @@ import ReportSelectDate from "../../Components/ReportSelectDate.vue";
import AppUserButton from "./AppUserButton.vue";
import {Link} from "@inertiajs/vue3";
import AppHeaderRole from "./AppHeaderRole.vue";
import {computed, useSlots} from "vue";
const slots = useSlots()
const hasHeaderExtra = computed(() => !!slots.headerExtra)
</script>
<template>
<div class="grid grid-cols-[auto_1fr_auto] px-4 w-full h-full">
<NButton :tag="Link" text href="/">
Метрика
</NButton>
<NSpace align="center">
<NButton :tag="Link" text href="/">
Метрика
</NButton>
<NDivider v-if="hasHeaderExtra" vertical />
<slot name="headerExtra" />
</NSpace>
<div></div>
<AppUserButton />
</div>

View File

@@ -54,7 +54,7 @@ const reportButtonType = computed(() => authStore.isDoctor ? 'button' : Link)
/>
<StartButton title="Статистика моего отделения"
:description="`Ваше отделение в системе: ${authStore.userDepartment.name_short}`"
:href="`/statistic?sent_at=${reportStore.timestampCurrentRange}&groupId=1`"
:href="`/statistic`"
:icon="TbChartTreemap"
/>
<StartButton title="Выйти из системы"

View File

@@ -1,5 +1,5 @@
<script setup>
import { NFlex, NButton } from 'naive-ui'
import { NFlex, NAlert, NButton } from 'naive-ui'
import ReportHeader from "./ReportHeader.vue";
import ReportFormInput from "./ReportFormInput.vue";
import ReportSection from "./ReportSection.vue";
@@ -28,6 +28,9 @@ const onSubmit = () => {
<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>
<ReportHeader :mode="mode" />
<ReportFormInput />

View File

@@ -5,6 +5,7 @@ import {useAuthStore} from "../../../Stores/auth.js";
const reportStore = useReportStore()
const authStore = useAuthStore()
</script>
<template>
@@ -66,4 +67,8 @@ const authStore = useAuthStore()
:deep(.n-statistic-value) {
@apply flex justify-center items-center;
}
:deep(.n-input-wrapper) {
width: 120px;
}
</style>

View File

@@ -66,7 +66,7 @@ const currentDate = computed(() => {
</NSpace>
<div class="col-3 w-full">
<ReportSelectDate />
<ReportSelectDate :is-one-day="reportStore.reportInfo.report?.isOneDay"/>
</div>
</div>

View File

@@ -41,7 +41,7 @@ const isReadonlyMode = computed(() => props.mode.toLowerCase() === 'readonly')
<template>
<NCard>
<NCollapse>
<NCollapse v-model:expanded-names="reportStore.openedCollapsible">
<NCollapseItem name="1">
<template #header>
<ReportSectionHeader title="Планово" status="plan" />

View File

@@ -1,12 +1,22 @@
<script setup>
import {NDataTable} from 'naive-ui'
import {NDataTable, NFlex, NText, NDatePicker} from 'naive-ui'
import AppLayout from "../../Layouts/AppLayout.vue";
import {ref} from "vue";
import {h, ref} from "vue";
import DatePickerQuery from "../../Components/DatePickerQuery.vue";
const props = defineProps({
data: {
type: Object,
default: []
},
isHeadOrAdmin: {
type: Boolean
},
date: {
type: [Number, Array]
},
isOneDay: {
type: Boolean
}
})
@@ -15,7 +25,17 @@ const columns = ref([
title: 'Отделение',
key: 'department',
width: 240,
titleAlign: 'center'
titleAlign: 'center',
colSpan: (row) => row.colspan,
render(row) {
if (row.isGroupHeader) {
return h(NFlex, {
align: "center",
justify: "center"
}, h(NText, { style: 'font-weight: 600;' }, row.groupName))
}
return row.department
}
},
{
title: 'Кол-во коек',
@@ -24,42 +44,35 @@ const columns = ref([
titleAlign: 'center',
align: 'center'
},
{
title: 'Состояло',
key: '',
width: 84,
titleAlign: 'center',
align: 'center'
},
{
title: 'Поступило',
key: 'received',
key: 'recipients',
titleAlign: 'center',
children: [
{
title: 'Всего',
key: 'all',
key: 'recipients.all',
width: 60,
titleAlign: 'center',
align: 'center'
},
{
title: 'План',
key: 'plan',
key: 'recipients.plan',
width: 60,
titleAlign: 'center',
align: 'center'
},
{
title: 'Экстр',
key: 'emergency',
key: 'recipients.emergency',
width: 60,
titleAlign: 'center',
align: 'center'
},
{
title: 'Перевод',
key: '',
key: 'recipients.transferred',
width: 84,
titleAlign: 'center',
align: 'center'
@@ -68,7 +81,7 @@ const columns = ref([
},
{
title: 'Выбыло',
key: 'leave',
key: 'outcome',
width: 84,
titleAlign: 'center',
align: 'center'
@@ -89,30 +102,46 @@ const columns = ref([
},
{
title: 'Операции',
key: '',
key: 'surgical',
titleAlign: 'center',
children: [
{
title: 'Э',
key: '',
key: 'surgical.emergency',
width: 60,
titleAlign: 'center',
align: 'center'
},
{
title: 'П',
key: '',
key: 'surgical.plan',
width: 60,
titleAlign: 'center',
align: 'center'
},
]
},
{
title: 'Умерло',
key: 'deceased',
width: 84,
titleAlign: 'center',
align: 'center'
},
])
const rowProps = (row) => {
if (row.isGroupHeader) return {
style: `--n-merged-td-color: var(--n-merged-th-color)`
}
}
</script>
<template>
<AppLayout>
<template #headerExtra>
<DatePickerQuery :is-head-or-admin="isHeadOrAdmin" :date="date" :is-one-day="isOneDay" />
</template>
<NDataTable :columns="columns"
:data="data"
size="small"
@@ -120,6 +149,7 @@ const columns = ref([
striped
min-height="calc(100vh - 48px - 70px)"
max-height="calc(100vh - 48px - 70px)"
:row-props="rowProps"
>
</NDataTable>

View File

@@ -23,6 +23,9 @@ export const useReportStore = defineStore('reportStore', () => {
const dataOnReport = ref(null)
// Открытие collapse из ReportSectionItem
const openedCollapsible = ref([])
const reportInfo = ref({
userId: null
})
@@ -85,6 +88,7 @@ export const useReportStore = defineStore('reportStore', () => {
unwantedEvents: unwantedEvents.value,
dates: timestampCurrentRange.value,
userId: reportInfo.value.userId,
reportId: reportInfo.value.report.report_id,
...assignForm
}
@@ -122,7 +126,7 @@ export const useReportStore = defineStore('reportStore', () => {
reportForm.value.metrika_item_3 = reportInfo.value.department?.recipientCount
reportForm.value.metrika_item_7 = reportInfo.value.department?.extractCount
reportForm.value.metrika_item_8 = reportInfo.value.department?.currentCounts
reportForm.value.metrika_item_8 = reportInfo.value.department?.currentCount
reportForm.value.metrika_item_9 = reportInfo.value.department?.deadCount
reportForm.value.metrika_item_10 = reportInfo.value.department?.surgicalCount[1]
@@ -143,6 +147,13 @@ export const useReportStore = defineStore('reportStore', () => {
const getDataOnReportDate = async (dateRange) => {
isLoadReportInfo.value = true
timestampCurrentRange.value = dateRange
openedCollapsible.value = []
patientsData.value = {
plan: [],
emergency: [],
observation: [],
outcome: []
}
const queryParams = {
userId: reportInfo.value.userId,
startAt: timestampCurrentRange.value[0],
@@ -185,6 +196,7 @@ export const useReportStore = defineStore('reportStore', () => {
reportForm,
departmentUsers,
unwantedEvents,
openedCollapsible,
getColumnsByKey,
getDataOnReportDate,

View File

@@ -0,0 +1,46 @@
import { format } from 'date-fns'
import { ru } from 'date-fns/locale'
/**
* Делает первую букву строки заглавной
*/
const capitalizeFirst = (str) => {
if (!str) return ''
return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
* Форматирует дату по-русски: "Вторник, 3 февраля 2026 г."
*/
export const formatRussianDate = (date) => {
if (!date) return ''
const formatted = format(new Date(date), 'EEEE, d MMMM yyyy г.', { locale: ru })
return capitalizeFirst(formatted)
}
/**
* Форматирует диапазон дат по-русски:
* "С понедельника, 2 февраля 2026 г. по вторник, 3 февраля 2026 г."
*/
export const formatRussianDateRange = (dateRange) => {
if (!dateRange || !Array.isArray(dateRange) || dateRange.length < 2) {
return ''
}
const [startDate, endDate] = dateRange
if (!startDate || !endDate) return ''
const formattedStart = format(new Date(startDate), 'd MMMM yyyy г.', { locale: ru })
const formattedEnd = format(new Date(endDate), 'd MMMM yyyy г.', { locale: ru })
return `С ${formattedStart.toLowerCase()} по ${formattedEnd.toLowerCase()}`
}
/**
* Для совместимости со старым кодом
*/
export const formatDateWithCapital = (date) => {
return formatRussianDate(date)
}