mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 03:02:15 +01:00
Fix DST-related timezone offset when creating/resizing/dragging calendar
events
This commit is contained in:
@@ -91,9 +91,7 @@ const emit = defineEmits<{
|
|||||||
:aria-label="dayEvent.event.title"
|
:aria-label="dayEvent.event.title"
|
||||||
role="button"
|
role="button"
|
||||||
@pointerdown="emit('event-pointerdown', $event, dayEvent)"
|
@pointerdown="emit('event-pointerdown', $event, dayEvent)"
|
||||||
@keydown.enter.prevent="
|
@keydown.enter.prevent="emit('event-keydown-enter', dayEvent)">
|
||||||
!dayEvent.event.isRunning && emit('event-keydown-enter', dayEvent)
|
|
||||||
">
|
|
||||||
<div
|
<div
|
||||||
v-if="!dayEvent.isClippedStart"
|
v-if="!dayEvent.isClippedStart"
|
||||||
class="fc-event-resizer fc-event-resizer-start absolute z-[99] w-full h-3 left-0 top-[-2px] cursor-row-resize flex items-center justify-center opacity-0 group-hover:opacity-100"
|
class="fc-event-resizer fc-event-resizer-start absolute z-[99] w-full h-3 left-0 top-[-2px] cursor-row-resize flex items-center justify-center opacity-0 group-hover:opacity-100"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ref, type Ref, type ComputedRef } from 'vue';
|
import { ref, type Ref, type ComputedRef } from 'vue';
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import type { TimeEntry } from '@/packages/api/src';
|
import type { TimeEntry } from '@/packages/api/src';
|
||||||
import { getDayJsInstance } from '../utils/time';
|
import { getDayJsInstance, getLocalizedDayJsFromMinutes } from '../utils/time';
|
||||||
import { getUserTimezone } from '../utils/settings';
|
|
||||||
import type { CalendarSettings } from './calendarSettings';
|
import type { CalendarSettings } from './calendarSettings';
|
||||||
import type { CalendarEvent } from './calendarTypes';
|
import type { CalendarEvent } from './calendarTypes';
|
||||||
|
|
||||||
@@ -34,11 +34,8 @@ export function useContextMenu(params: {
|
|||||||
const snap = params.calendarSettings.value.snapMinutes;
|
const snap = params.calendarSettings.value.snapMinutes;
|
||||||
const snappedMinutes = Math.floor(minutesFromGridStart / snap) * snap;
|
const snappedMinutes = Math.floor(minutesFromGridStart / snap) * snap;
|
||||||
|
|
||||||
const dayjs = getDayJsInstance();
|
const startLocal = getLocalizedDayJsFromMinutes(date, snappedMinutes);
|
||||||
const startLocal = dayjs(`${date}T00:00:00`)
|
const snappedEnd = getLocalizedDayJsFromMinutes(date, snappedMinutes + snap);
|
||||||
.tz(getUserTimezone(), true)
|
|
||||||
.add(snappedMinutes, 'minute');
|
|
||||||
const snappedEnd = startLocal.add(snap, 'minute');
|
|
||||||
|
|
||||||
return { start: startLocal.utc(), end: snappedEnd.utc() };
|
return { start: startLocal.utc(), end: snappedEnd.utc() };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { computed, ref, onUnmounted, type Ref, type ComputedRef } from 'vue';
|
import { computed, ref, onUnmounted, type Ref, type ComputedRef } from 'vue';
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import type { TimeEntry } from '@/packages/api/src';
|
import type { TimeEntry } from '@/packages/api/src';
|
||||||
import { getDayJsInstance, getLocalizedDayJs } from '../utils/time';
|
import { getLocalizedDayJs, getLocalizedDayJsFromMinutes } from '../utils/time';
|
||||||
import { getUserTimezone } from '../utils/settings';
|
|
||||||
import type { CalendarSettings } from './calendarSettings';
|
import type { CalendarSettings } from './calendarSettings';
|
||||||
import type { CalendarEvent, DayEvent } from './calendarTypes';
|
import type { CalendarEvent, DayEvent } from './calendarTypes';
|
||||||
import { SLOT_HEIGHT, DRAG_THRESHOLD } from './calendarTypes';
|
import { SLOT_HEIGHT, DRAG_THRESHOLD } from './calendarTypes';
|
||||||
@@ -67,8 +66,7 @@ export function useEventDrag(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dayEvent.isClippedStart && originDay && ev.timeEntry.end) {
|
if (dayEvent.isClippedStart && originDay && ev.timeEntry.end) {
|
||||||
const dayjs = getDayJsInstance();
|
const dayMidnight = getLocalizedDayJsFromMinutes(originDay, 0);
|
||||||
const dayMidnight = dayjs(`${originDay}T00:00:00`).tz(getUserTimezone(), true);
|
|
||||||
const evStart = getLocalizedDayJs(ev.timeEntry.start);
|
const evStart = getLocalizedDayJs(ev.timeEntry.start);
|
||||||
const eventStartFromGridStart = evStart.diff(dayMidnight, 'minute') - s.startHour * 60;
|
const eventStartFromGridStart = evStart.diff(dayMidnight, 'minute') - s.startHour * 60;
|
||||||
const segmentTopMinutes = (dayEvent.top / SLOT_HEIGHT) * s.slotMinutes;
|
const segmentTopMinutes = (dayEvent.top / SLOT_HEIGHT) * s.slotMinutes;
|
||||||
@@ -154,13 +152,11 @@ export function useEventDrag(params: {
|
|||||||
const lowerBound = startMin - 4 * 60;
|
const lowerBound = startMin - 4 * 60;
|
||||||
const clampedMinutes = Math.max(lowerBound, Math.min(snappedMinutes, s.endHour * 60));
|
const clampedMinutes = Math.max(lowerBound, Math.min(snappedMinutes, s.endHour * 60));
|
||||||
|
|
||||||
const dayjs = getDayJsInstance();
|
const originalSegmentStart = getLocalizedDayJsFromMinutes(
|
||||||
const originalSegmentStart = dayjs(`${savedOriginalDayStr}T00:00:00`)
|
savedOriginalDayStr,
|
||||||
.tz(getUserTimezone(), true)
|
startMin + params.pixelsToMinutesFromMidnight(dragStartEventTop)
|
||||||
.add(startMin + params.pixelsToMinutesFromMidnight(dragStartEventTop), 'minute');
|
);
|
||||||
const newSegmentStart = dayjs(`${targetDateStr}T00:00:00`)
|
const newSegmentStart = getLocalizedDayJsFromMinutes(targetDateStr, clampedMinutes);
|
||||||
.tz(getUserTimezone(), true)
|
|
||||||
.add(clampedMinutes, 'minute');
|
|
||||||
const deltaMs = newSegmentStart.diff(originalSegmentStart);
|
const deltaMs = newSegmentStart.diff(originalSegmentStart);
|
||||||
|
|
||||||
const origStart = getLocalizedDayJs(timeEntry.start);
|
const origStart = getLocalizedDayJs(timeEntry.start);
|
||||||
@@ -240,11 +236,14 @@ export function useEventDrag(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Multi-day: compute actual start/end datetimes, then clip per day
|
// Multi-day: compute actual start/end datetimes, then clip per day
|
||||||
const dayjs = getDayJsInstance();
|
const eventStartAbsolute = getLocalizedDayJsFromMinutes(
|
||||||
const eventStartAbsolute = dayjs(`${dragCurrentDay.value}T00:00:00`)
|
dragCurrentDay.value,
|
||||||
.tz(getUserTimezone(), true)
|
startMin + eventStartOnGrid
|
||||||
.add(startMin + eventStartOnGrid, 'minute');
|
);
|
||||||
const eventEndAbsolute = eventStartAbsolute.add(dragFullDurationMinutes, 'minute');
|
const eventEndAbsolute = getLocalizedDayJsFromMinutes(
|
||||||
|
dragCurrentDay.value,
|
||||||
|
startMin + eventStartOnGrid + dragFullDurationMinutes
|
||||||
|
);
|
||||||
|
|
||||||
const result: Record<string, Record<string, string>> = {};
|
const result: Record<string, Record<string, string>> = {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { computed, ref, onUnmounted, type Ref, type ComputedRef } from 'vue';
|
import { computed, ref, onUnmounted, type Ref, type ComputedRef } from 'vue';
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import type { TimeEntry } from '@/packages/api/src';
|
import type { TimeEntry } from '@/packages/api/src';
|
||||||
import { getDayJsInstance, getLocalizedDayJs } from '../utils/time';
|
import { getDayJsInstance, getLocalizedDayJs, getLocalizedDayJsFromMinutes } from '../utils/time';
|
||||||
import { getUserTimezone } from '../utils/settings';
|
|
||||||
import type { CalendarSettings } from './calendarSettings';
|
import type { CalendarSettings } from './calendarSettings';
|
||||||
import type { CalendarEvent, DayEvent } from './calendarTypes';
|
import type { CalendarEvent, DayEvent } from './calendarTypes';
|
||||||
import { SLOT_HEIGHT } from './calendarTypes';
|
import { SLOT_HEIGHT } from './calendarTypes';
|
||||||
@@ -11,10 +10,6 @@ function snapTo(value: number, step: number): number {
|
|||||||
return Math.round(value / step) * step;
|
return Math.round(value / step) * step;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dayMidnightLocal(dayStr: string): Dayjs {
|
|
||||||
return getDayJsInstance()(`${dayStr}T00:00:00`).tz(getUserTimezone(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useEventResize(params: {
|
export function useEventResize(params: {
|
||||||
calendarSettings: Ref<CalendarSettings>;
|
calendarSettings: Ref<CalendarSettings>;
|
||||||
viewDays: ComputedRef<Dayjs[]>;
|
viewDays: ComputedRef<Dayjs[]>;
|
||||||
@@ -89,7 +84,7 @@ export function useEventResize(params: {
|
|||||||
),
|
),
|
||||||
s.snapMinutes
|
s.snapMinutes
|
||||||
);
|
);
|
||||||
return { start, end: dayMidnightLocal(endDay).add(endMinutes, 'minute') };
|
return { start, end: getLocalizedDayJsFromMinutes(endDay, endMinutes) };
|
||||||
} else {
|
} else {
|
||||||
const end = resizeOriginalEvent.isRunning
|
const end = resizeOriginalEvent.isRunning
|
||||||
? getLocalizedDayJs()
|
? getLocalizedDayJs()
|
||||||
@@ -105,7 +100,7 @@ export function useEventResize(params: {
|
|||||||
params.pixelsToMinutesFromMidnight(resizeCurrentTop.value),
|
params.pixelsToMinutesFromMidnight(resizeCurrentTop.value),
|
||||||
s.snapMinutes
|
s.snapMinutes
|
||||||
);
|
);
|
||||||
return { start: dayMidnightLocal(startDay).add(startMinutes, 'minute'), end };
|
return { start: getLocalizedDayJsFromMinutes(startDay, startMinutes), end };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { computed, ref, onUnmounted, type Ref, type ComputedRef } from 'vue';
|
import { computed, ref, onUnmounted, type Ref, type ComputedRef } from 'vue';
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import { getDayJsInstance } from '../utils/time';
|
import { getLocalizedDayJsFromMinutes } from '../utils/time';
|
||||||
import { getUserTimezone } from '../utils/settings';
|
|
||||||
import type { CalendarSettings } from './calendarSettings';
|
import type { CalendarSettings } from './calendarSettings';
|
||||||
import { SLOT_HEIGHT } from './calendarTypes';
|
import { SLOT_HEIGHT } from './calendarTypes';
|
||||||
|
|
||||||
@@ -102,8 +102,6 @@ export function useSlotSelection(params: {
|
|||||||
|
|
||||||
const s = params.calendarSettings.value;
|
const s = params.calendarSettings.value;
|
||||||
const snap = s.snapMinutes;
|
const snap = s.snapMinutes;
|
||||||
const dayjs = getDayJsInstance();
|
|
||||||
|
|
||||||
const startMinutes = params.pixelsToMinutesFromMidnight(selectionTop.value);
|
const startMinutes = params.pixelsToMinutesFromMidnight(selectionTop.value);
|
||||||
const snappedStartMin = Math.floor(startMinutes / snap) * snap;
|
const snappedStartMin = Math.floor(startMinutes / snap) * snap;
|
||||||
|
|
||||||
@@ -138,12 +136,8 @@ export function useSlotSelection(params: {
|
|||||||
if (endMin <= 0) endMin = snap;
|
if (endMin <= 0) endMin = snap;
|
||||||
}
|
}
|
||||||
|
|
||||||
startLocal = dayjs(`${startDateStr}T00:00:00`)
|
startLocal = getLocalizedDayJsFromMinutes(startDateStr, startMin);
|
||||||
.tz(getUserTimezone(), true)
|
endLocal = getLocalizedDayJsFromMinutes(endDateStr, endMin);
|
||||||
.add(startMin, 'minute');
|
|
||||||
endLocal = dayjs(`${endDateStr}T00:00:00`)
|
|
||||||
.tz(getUserTimezone(), true)
|
|
||||||
.add(endMin, 'minute');
|
|
||||||
} else {
|
} else {
|
||||||
const startDateStr = selectionStartDay;
|
const startDateStr = selectionStartDay;
|
||||||
const endMinutes = params.pixelsToMinutesFromMidnight(
|
const endMinutes = params.pixelsToMinutesFromMidnight(
|
||||||
@@ -153,12 +147,8 @@ export function useSlotSelection(params: {
|
|||||||
if (snappedEndMin <= snappedStartMin) {
|
if (snappedEndMin <= snappedStartMin) {
|
||||||
snappedEndMin = snappedStartMin + snap;
|
snappedEndMin = snappedStartMin + snap;
|
||||||
}
|
}
|
||||||
startLocal = dayjs(`${startDateStr}T00:00:00`)
|
startLocal = getLocalizedDayJsFromMinutes(startDateStr, snappedStartMin);
|
||||||
.tz(getUserTimezone(), true)
|
endLocal = getLocalizedDayJsFromMinutes(startDateStr, snappedEndMin);
|
||||||
.add(snappedStartMin, 'minute');
|
|
||||||
endLocal = dayjs(`${startDateStr}T00:00:00`)
|
|
||||||
.tz(getUserTimezone(), true)
|
|
||||||
.add(snappedEndMin, 'minute');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
params.onSelectionComplete(startLocal.utc(), endLocal.utc());
|
params.onSelectionComplete(startLocal.utc(), endLocal.utc());
|
||||||
|
|||||||
@@ -147,6 +147,18 @@ export function getLocalizedDayJs(timestamp?: string | null) {
|
|||||||
return dayjs.utc(timestamp).tz(getUserTimezone());
|
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) {
|
export function getLocalizedDateFromTimestamp(timestamp: string) {
|
||||||
return getLocalizedDayJs(timestamp).format('YYYY-MM-DD');
|
return getLocalizedDayJs(timestamp).format('YYYY-MM-DD');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user