diff --git a/resources/js/packages/ui/src/FullCalendar/CalendarDayColumn.vue b/resources/js/packages/ui/src/FullCalendar/CalendarDayColumn.vue index 3b24b40b..d23bc0a2 100644 --- a/resources/js/packages/ui/src/FullCalendar/CalendarDayColumn.vue +++ b/resources/js/packages/ui/src/FullCalendar/CalendarDayColumn.vue @@ -91,9 +91,7 @@ const emit = defineEmits<{ :aria-label="dayEvent.event.title" role="button" @pointerdown="emit('event-pointerdown', $event, dayEvent)" - @keydown.enter.prevent=" - !dayEvent.event.isRunning && emit('event-keydown-enter', dayEvent) - "> + @keydown.enter.prevent="emit('event-keydown-enter', dayEvent)">
> = {}; diff --git a/resources/js/packages/ui/src/FullCalendar/useEventResize.ts b/resources/js/packages/ui/src/FullCalendar/useEventResize.ts index dbb46406..a7926539 100644 --- a/resources/js/packages/ui/src/FullCalendar/useEventResize.ts +++ b/resources/js/packages/ui/src/FullCalendar/useEventResize.ts @@ -1,8 +1,7 @@ import { computed, ref, onUnmounted, type Ref, type ComputedRef } from 'vue'; import type { Dayjs } from 'dayjs'; import type { TimeEntry } from '@/packages/api/src'; -import { getDayJsInstance, getLocalizedDayJs } from '../utils/time'; -import { getUserTimezone } from '../utils/settings'; +import { getDayJsInstance, getLocalizedDayJs, getLocalizedDayJsFromMinutes } from '../utils/time'; import type { CalendarSettings } from './calendarSettings'; import type { CalendarEvent, DayEvent } from './calendarTypes'; import { SLOT_HEIGHT } from './calendarTypes'; @@ -11,10 +10,6 @@ function snapTo(value: number, step: number): number { return Math.round(value / step) * step; } -function dayMidnightLocal(dayStr: string): Dayjs { - return getDayJsInstance()(`${dayStr}T00:00:00`).tz(getUserTimezone(), true); -} - export function useEventResize(params: { calendarSettings: Ref; viewDays: ComputedRef; @@ -89,7 +84,7 @@ export function useEventResize(params: { ), s.snapMinutes ); - return { start, end: dayMidnightLocal(endDay).add(endMinutes, 'minute') }; + return { start, end: getLocalizedDayJsFromMinutes(endDay, endMinutes) }; } else { const end = resizeOriginalEvent.isRunning ? getLocalizedDayJs() @@ -105,7 +100,7 @@ export function useEventResize(params: { params.pixelsToMinutesFromMidnight(resizeCurrentTop.value), s.snapMinutes ); - return { start: dayMidnightLocal(startDay).add(startMinutes, 'minute'), end }; + return { start: getLocalizedDayJsFromMinutes(startDay, startMinutes), end }; } } diff --git a/resources/js/packages/ui/src/FullCalendar/useSlotSelection.ts b/resources/js/packages/ui/src/FullCalendar/useSlotSelection.ts index 1b5abfa0..9b462c98 100644 --- a/resources/js/packages/ui/src/FullCalendar/useSlotSelection.ts +++ b/resources/js/packages/ui/src/FullCalendar/useSlotSelection.ts @@ -1,7 +1,7 @@ import { computed, ref, onUnmounted, type Ref, type ComputedRef } from 'vue'; import type { Dayjs } from 'dayjs'; -import { getDayJsInstance } from '../utils/time'; -import { getUserTimezone } from '../utils/settings'; +import { getLocalizedDayJsFromMinutes } from '../utils/time'; + import type { CalendarSettings } from './calendarSettings'; import { SLOT_HEIGHT } from './calendarTypes'; @@ -102,8 +102,6 @@ export function useSlotSelection(params: { const s = params.calendarSettings.value; const snap = s.snapMinutes; - const dayjs = getDayJsInstance(); - const startMinutes = params.pixelsToMinutesFromMidnight(selectionTop.value); const snappedStartMin = Math.floor(startMinutes / snap) * snap; @@ -138,12 +136,8 @@ export function useSlotSelection(params: { if (endMin <= 0) endMin = snap; } - startLocal = dayjs(`${startDateStr}T00:00:00`) - .tz(getUserTimezone(), true) - .add(startMin, 'minute'); - endLocal = dayjs(`${endDateStr}T00:00:00`) - .tz(getUserTimezone(), true) - .add(endMin, 'minute'); + startLocal = getLocalizedDayJsFromMinutes(startDateStr, startMin); + endLocal = getLocalizedDayJsFromMinutes(endDateStr, endMin); } else { const startDateStr = selectionStartDay; const endMinutes = params.pixelsToMinutesFromMidnight( @@ -153,12 +147,8 @@ export function useSlotSelection(params: { if (snappedEndMin <= snappedStartMin) { snappedEndMin = snappedStartMin + snap; } - startLocal = dayjs(`${startDateStr}T00:00:00`) - .tz(getUserTimezone(), true) - .add(snappedStartMin, 'minute'); - endLocal = dayjs(`${startDateStr}T00:00:00`) - .tz(getUserTimezone(), true) - .add(snappedEndMin, 'minute'); + startLocal = getLocalizedDayJsFromMinutes(startDateStr, snappedStartMin); + endLocal = getLocalizedDayJsFromMinutes(startDateStr, snappedEndMin); } params.onSelectionComplete(startLocal.utc(), endLocal.utc()); diff --git a/resources/js/packages/ui/src/utils/time.ts b/resources/js/packages/ui/src/utils/time.ts index 366c75d7..ffef06dd 100644 --- a/resources/js/packages/ui/src/utils/time.ts +++ b/resources/js/packages/ui/src/utils/time.ts @@ -147,6 +147,18 @@ export function getLocalizedDayJs(timestamp?: string | null) { return dayjs.utc(timestamp).tz(getUserTimezone()); } +/** + * Create a dayjs instance for a specific wall-clock time on a given day. + * Sets hour/minute directly to avoid DST issues with .add() on transition days. + */ +export function getLocalizedDayJsFromMinutes(dayStr: string, minutesFromMidnight: number) { + return dayjs + .tz(`${dayStr}T00:00:00`, getUserTimezone()) + .hour(Math.floor(minutesFromMidnight / 60)) + .minute(Math.round(minutesFromMidnight % 60)) + .second(0); +} + export function getLocalizedDateFromTimestamp(timestamp: string) { return getLocalizedDayJs(timestamp).format('YYYY-MM-DD'); }