Обновлен стартовый экран

Переписаны запросы для статистики, отчетов
Добавлена интеграция отчета сестры
This commit is contained in:
brusnitsyn
2026-05-28 22:10:00 +09:00
parent 90e0d04dfd
commit 739168d427
96 changed files with 6663 additions and 1465 deletions

View File

@@ -1,11 +1,11 @@
<script setup>
import {NDatePicker, NIcon, NFlex, NButton, NSkeleton} from 'naive-ui'
import {computed, onMounted, ref, watch} from "vue";
import {computed, ref} from "vue";
import {router} from "@inertiajs/vue3";
import {TbCalendar} from 'vue-icons-plus/tb'
import {formatRussianDate, formatRussianDateRange} from "../Utils/dateFormatter.js";
import {useReportStore} from "../Stores/report.js";
import {formatDistanceStrict, getTime} from "date-fns";
import {formatDistanceStrict} from "date-fns";
const props = defineProps({
// Пользователь с ролью админ или зав
@@ -16,9 +16,6 @@ const props = defineProps({
date: {
type: [Number, Array]
},
isOneDay: {
type: Boolean
},
isShowCurrentDateSwitch: {
type: Boolean,
default: false
@@ -27,113 +24,203 @@ const props = defineProps({
const reportStore = useReportStore()
const isLoading = ref(false)
const isUseDateRange = computed(() => props.isHeadOrAdmin)
const datePicker = ref()
const showCalendar = ref(false)
const calendarType = ref(props.isHeadOrAdmin ? 'daterange' : 'date')
const dateType = computed(() => {
if (isUseDateRange.value) return 'daterange'
return 'date'
})
const isDateRange = computed(() => calendarType.value === 'daterange')
const isDateIsOneDay = computed(() => {
return formatDistanceStrict(modelValue.value[0], modelValue.value[1]) === '0 seconds' || formatDistanceStrict(modelValue.value[0], modelValue.value[1]) === '1 day'
})
const queryDate = ref([null, null])
const modelValue = defineModel('date')
const onChangeDate = () => {
if (isDateIsOneDay.value) {
const startDate = Date.parse(new Date(new Date().getFullYear(), 0, 1).toDateString())
const date = Date.parse(new Date().toDateString())
const arrayDate = [startDate, date]
handleDateUpdate(arrayDate)
} else {
const date = Date.parse(new Date().toDateString())
const arrayDate = [date, date]
handleDateUpdate(arrayDate)
}
showCalendar.value = false
}
const setQueryDate = () => {
reportStore.openedCollapsible = []
router.reload({
data: {
startAt: queryDate.value[0],
endAt: queryDate.value[1]
},
onFinish: () => {
isLoading.value = false
}
})
}
// Всегда используем диапазон, но для врача показываем как один день
const calendarType = 'daterange'
// Форматированное значение для отображения
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)
}
if (!value || !Array.isArray(value) || !value[0] || !value[1]) {
return ''
}
// Для админа/зав. показываем диапазон
if (props.isHeadOrAdmin) {
return formatRussianDateRange(value)
}
// Для врача показываем одну дату (конец смены)
return formatRussianDate(value[1])
})
const handleDateUpdate = (value) => {
isLoading.value = true
// Устанавливаем новое значение
modelValue.value = value
// Проверяем, выбран ли один день
const isOneDaySelected = computed(() => {
if (!modelValue.value || !Array.isArray(modelValue.value)) return false
const [start, end] = modelValue.value
if (!start || !end) return false
// Для диапазона: отправляем только если обе даты заполнены
if (isDateRange.value) {
if (Array.isArray(value) && value[0] && value[1]) {
// Дебаунс для предотвращения двойной отправки
setTimeout(() => {
// Проверяем что значение не изменилось за время таймаута
if (JSON.stringify(modelValue.value) === JSON.stringify(value)) {
queryDate.value = value
setQueryDate()
}
}, 100)
}
const diff = formatDistanceStrict(start, end)
return diff === '0 seconds' || diff === '1 day'
})
// Блокировка будущих дат с учетом смен 09:00-09:00
const isDateDisabled = (ts) => {
const date = new Date(ts)
const now = new Date()
// Получаем сегодняшнюю дату без времени
const today = new Date()
today.setHours(0, 0, 0, 0)
// Если выбранная дата меньше сегодня - разрешаем (прошлые даты)
if (date < today) {
return false
}
// Получаем текущий час
const currentHour = now.getHours()
// Если сейчас меньше 09:00, то максимальная дата - сегодня
// Если сейчас 09:00 или больше, то максимальная дата - завтра
const maxDaysOffset = currentHour < 9 ? 0 : 1
const maxDate = new Date()
maxDate.setDate(maxDate.getDate() + maxDaysOffset)
maxDate.setHours(23, 59, 59, 999)
// Блокируем только даты строго больше максимальной
return date > maxDate
}
// Получить начало смены для даты
const getShiftStart = (date) => {
const shiftStart = new Date(date)
shiftStart.setHours(9, 0, 0, 0)
return shiftStart
}
// Получить конец смены для даты
const getShiftEnd = (date) => {
const shiftEnd = new Date(date)
shiftEnd.setHours(9, 0, 0, 0)
shiftEnd.setDate(shiftEnd.getDate() + 1)
return shiftEnd
}
// Получить текущую смену
const getCurrentShift = () => {
const now = new Date()
const today9am = new Date()
today9am.setHours(9, 0, 0, 0)
if (now < today9am) {
const start = new Date(today9am)
start.setDate(start.getDate() - 1)
return [start, today9am]
} else {
// Для одиночной даты отправляем сразу
if (value) {
queryDate.value = [value, value]
setQueryDate()
}
const end = new Date(today9am)
end.setDate(end.getDate() + 1)
return [today9am, end]
}
}
// Получить предыдущую смену
const getPreviousShift = () => {
const now = new Date()
const today9am = new Date()
today9am.setHours(9, 0, 0, 0)
if (now < today9am) {
const start = new Date(today9am)
start.setDate(start.getDate() - 2)
const end = new Date(today9am)
end.setDate(end.getDate() - 1)
return [start, end]
} else {
const start = new Date(today9am)
start.setDate(start.getDate() - 1)
return [start, today9am]
}
}
// Шорткаты
const shortcuts = {
'Сегодня': () => getCurrentShift(),
'Вчера': () => getPreviousShift(),
'За 7 дней': () => {
const currentShift = getCurrentShift()
const end = currentShift[1]
const start = new Date(end)
start.setDate(start.getDate() - 7)
start.setHours(9, 0, 0, 0)
return [start, end]
},
'За 30 дней': () => {
const currentShift = getCurrentShift()
const end = currentShift[1]
const start = new Date(end)
start.setDate(start.getDate() - 30)
start.setHours(9, 0, 0, 0)
return [start, end]
},
'Текущий год': () => {
const currentShift = getCurrentShift()
const end = currentShift[1]
const start = new Date(end.getFullYear(), 0, 1)
start.setHours(9, 0, 0, 0)
return [start, end]
}
}
// Обработка изменения даты
const handleDateUpdate = (value) => {
if (!value || !Array.isArray(value) || !value[0] || !value[1]) return
isLoading.value = true
let finalValue = value
// Для врача: если выбран диапазон, преобразуем в один день (берем конец смены)
if (!props.isHeadOrAdmin) {
// Если выбрано несколько дней, берем последний день
finalValue = [value[1], value[1]]
}
modelValue.value = finalValue
// Дебаунс для предотвращения двойной отправки
setTimeout(() => {
if (JSON.stringify(modelValue.value) === JSON.stringify(finalValue)) {
reportStore.openedCollapsible = []
router.reload({
data: {
startAt: finalValue[0],
endAt: finalValue[1]
},
onFinish: () => {
isLoading.value = false
}
})
}
}, 100)
}
// Кнопка "Сегодня/Текущий год"
const onQuickSelect = () => {
const currentShift = getCurrentShift()
if (isOneDaySelected.value) {
// Если выбран один день, переключаем на текущий год
const end = currentShift[1]
const start = new Date(end.getFullYear(), 0, 1)
start.setHours(9, 0, 0, 0)
handleDateUpdate([start, end])
} else {
// Если выбран диапазон, переключаем на сегодня
handleDateUpdate(currentShift)
}
showCalendar.value = false
}
// Текст для кнопки
const quickButtonText = computed(() => {
return isOneDaySelected.value ? 'Текущий год' : 'Сегодня'
})
</script>
<template>
@@ -142,30 +229,28 @@ const handleDateUpdate = (value) => {
<NSkeleton width="240" height="20" round />
</template>
<template v-else>
<div v-if="formattedValue" class="font-medium leading-3 cursor-pointer" @click="showCalendar = true">
<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="value => handleDateUpdate(value)"
:type="calendarType">
<template #confirm="{onConfirm, disabled, text}">
<NFlex justify="end" align="center" :size="0">
<NButton v-if="isShowCurrentDateSwitch" secondary type="primary" size="tiny" @click="onChangeDate()">
{{ isDateIsOneDay ? 'Текущий год' : 'Сегодня' }}
</NButton>
<NButton type="success" size="tiny" @click="onConfirm" :disabled="disabled">
{{ text }}
</NButton>
</NFlex>
</template>
<NDatePicker
v-model:value="modelValue"
v-model:show="showCalendar"
class="opacity-0 absolute! inset-x-0 bottom-full -translate-y-1/2"
placement="top-start"
:shortcuts="shortcuts"
:is-date-disabled="isDateDisabled"
input-readonly
bind-calendar-months
type="daterange"
@update:value="handleDateUpdate"
>
</NDatePicker>
<div class="cursor-pointer p-2 flex items-center justify-center" @click="showCalendar = true">
<NIcon size="20">
<TbCalendar />