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');
}