269 lines
8.3 KiB
Vue
269 lines
8.3 KiB
Vue
<script setup>
|
||
import {NDatePicker, NIcon, NFlex, NButton, NSkeleton} from 'naive-ui'
|
||
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} from "date-fns";
|
||
|
||
const props = defineProps({
|
||
// Пользователь с ролью админ или зав
|
||
isHeadOrAdmin: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
date: {
|
||
type: [Number, Array]
|
||
},
|
||
isShowCurrentDateSwitch: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
})
|
||
|
||
const reportStore = useReportStore()
|
||
const isLoading = ref(false)
|
||
const showCalendar = ref(false)
|
||
const modelValue = defineModel('date')
|
||
|
||
// Всегда используем диапазон, но для врача показываем как один день
|
||
const calendarType = 'daterange'
|
||
|
||
// Форматированное значение для отображения
|
||
const formattedValue = computed(() => {
|
||
const value = modelValue.value
|
||
|
||
if (!value || !Array.isArray(value) || !value[0] || !value[1]) {
|
||
return ''
|
||
}
|
||
|
||
// Для админа/зав. показываем диапазон
|
||
if (props.isHeadOrAdmin) {
|
||
return formatRussianDateRange(value)
|
||
}
|
||
|
||
// Для врача показываем одну дату (конец смены)
|
||
return formatRussianDate(value[1])
|
||
})
|
||
|
||
// Проверяем, выбран ли один день
|
||
const isOneDaySelected = computed(() => {
|
||
if (!modelValue.value || !Array.isArray(modelValue.value)) return false
|
||
const [start, end] = modelValue.value
|
||
if (!start || !end) return false
|
||
|
||
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 {
|
||
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>
|
||
<div class="relative inline-flex items-center">
|
||
<template v-if="isLoading">
|
||
<NSkeleton width="240" height="20" round />
|
||
</template>
|
||
<template v-else>
|
||
<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"
|
||
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 />
|
||
</NIcon>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
:deep(.n-input) {
|
||
position: absolute;
|
||
pointer-events: none;
|
||
}
|
||
</style>
|