diff --git a/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue b/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue index 1a561949..94d1ad8a 100644 --- a/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue +++ b/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue @@ -441,6 +441,13 @@ function snapEndToGrid(time: Dayjs, snapMinutes: number): Dayjs { return time.startOf('day').add(snapped, 'minute'); } +// Snap a dayjs time to the nearest snap boundary (for resize) +function snapToNearestGrid(time: Dayjs, snapMinutes: number): Dayjs { + const minutes = time.hour() * 60 + time.minute(); + const snapped = Math.round(minutes / snapMinutes) * snapMinutes; + return time.startOf('day').add(snapped, 'minute'); +} + // --- Visual snap (composable) --- const { startDragSnap: startVisualDragSnap, @@ -503,9 +510,11 @@ async function handleEventResize(arg: EventChangeArg) { const startChanged = !newStartLocal.isSame(origStartLocal, 'minute'); - // Snap only the changed edge once, reuse for both setDates and API update - const snappedStart = startChanged ? snapStartToGrid(newStartLocal, snap) : null; - const snappedEnd = !startChanged && !ext.isRunning ? snapEndToGrid(newEndLocal, snap) : null; + // Snap only the changed edge once, reuse for both setDates and API update. + // Use Math.round (snapToNearestGrid) to match the visual snap during resize. + const snappedStart = startChanged ? snapToNearestGrid(newStartLocal, snap) : null; + const snappedEnd = + !startChanged && !ext.isRunning ? snapToNearestGrid(newEndLocal, snap) : null; // Set FC event to snapped position immediately to avoid flash. // Use the original event date for the edge that wasn't resized. diff --git a/resources/js/packages/ui/src/FullCalendar/useVisualSnap.ts b/resources/js/packages/ui/src/FullCalendar/useVisualSnap.ts index 438ca4ef..fdc64ec1 100644 --- a/resources/js/packages/ui/src/FullCalendar/useVisualSnap.ts +++ b/resources/js/packages/ui/src/FullCalendar/useVisualSnap.ts @@ -159,12 +159,12 @@ export function useVisualSnap({ } if (resizeEdge === 'bottom') { - const snappedEnd = Math.ceil(endPos / snapPx) * snapPx; + const snappedEnd = Math.round(endPos / snapPx) * snapPx; const clampedEnd = Math.max(top + snapPx, snappedEnd); harness.style.bottom = -clampedEnd + 'px'; if (mirror) updateMirrorDurationLabel(mirror, top, clampedEnd, snapPx); } else if (resizeEdge === 'top') { - const snappedTop = Math.floor(top / snapPx) * snapPx; + const snappedTop = Math.round(top / snapPx) * snapPx; const clampedTop = Math.min(endPos - snapPx, snappedTop); harness.style.top = clampedTop + 'px'; if (mirror) updateMirrorDurationLabel(mirror, clampedTop, endPos, snapPx);