From 81f4d976cd4ab0771126ed92d9262c9baa79d5de Mon Sep 17 00:00:00 2001 From: "i.seliverstov" Date: Fri, 14 Aug 2026 21:13:14 +0500 Subject: [PATCH] fix(calendar): keep real events when switching day/week view Day view opened on the viewport center and week restore scrolled to that day, so recurring tasks with dueDate on today rendered only as ghosts. Co-authored-by: Cursor --- .../calendar/lib/calendar-events.spec.ts | 29 ++++++++++ .../calendar/lib/use-infinite-days.ts | 24 +++++++- .../src/features/calendar/lib/week.spec.ts | 36 ++++++++++++ .../src/features/calendar/lib/week.ts | 17 +++++- .../features/calendar/ui/WeeklyCalendar.vue | 55 +++++++++++++------ 5 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 alfy-bot-frontend/src/features/calendar/lib/week.spec.ts diff --git a/alfy-bot-frontend/src/features/calendar/lib/calendar-events.spec.ts b/alfy-bot-frontend/src/features/calendar/lib/calendar-events.spec.ts index c3fd923..b516d7a 100644 --- a/alfy-bot-frontend/src/features/calendar/lib/calendar-events.spec.ts +++ b/alfy-bot-frontend/src/features/calendar/lib/calendar-events.spec.ts @@ -279,4 +279,33 @@ describe('tasksToCalendarEvents — occupied series slots / ghosts from root', ( const events = tasksToCalendarEvents([root, overdue], weekStart, weekEnd) expect(events.filter(e => e.isVirtual)).toHaveLength(0) }) + + it('однодневное окно (day view) показывает реальный timed event, не ghost', () => { + const due = local(2026, 4, 29, 10, 0) + const task = baseTask({ + id: 'daily-1', + dueDate: due, + recurrence: dailyRule, + }) + const start = local(2026, 4, 29, 0, 0) + const end = local(2026, 4, 29, 23, 59) + + const events = tasksToCalendarEvents([task], start, end) + expect(events.filter(e => !e.isVirtual)).toHaveLength(1) + expect(events.filter(e => e.isVirtual)).toHaveLength(0) + }) + + it('день без dueDate у daily-серии — только ghost (центр вьюпорта ≠ сегодня)', () => { + const task = baseTask({ + id: 'daily-1', + dueDate: local(2026, 4, 29, 10, 0), + recurrence: dailyRule, + }) + const start = local(2026, 5, 1, 0, 0) + const end = local(2026, 5, 1, 23, 59) + + const events = tasksToCalendarEvents([task], start, end) + expect(events.filter(e => !e.isVirtual)).toHaveLength(0) + expect(events.filter(e => e.isVirtual)).toHaveLength(1) + }) }) diff --git a/alfy-bot-frontend/src/features/calendar/lib/use-infinite-days.ts b/alfy-bot-frontend/src/features/calendar/lib/use-infinite-days.ts index 5727995..f04bb9a 100644 --- a/alfy-bot-frontend/src/features/calendar/lib/use-infinite-days.ts +++ b/alfy-bot-frontend/src/features/calendar/lib/use-infinite-days.ts @@ -60,9 +60,20 @@ export function useInfiniteDays(gridRef: Ref) { }) } + function captureScrollLeft(): number { + return scrollLeft.value + } + + function restoreScrollLeft(left: number) { + scrollLeft.value = left + if (gridRef.value) + gridRef.value.scrollLeft = left + } + function scrollToDate(date: Date, smooth = false) { if (!gridRef.value) return const target = dayOffset(date) + scrollLeft.value = target gridRef.value.scrollTo({ left: target, behavior: smooth ? 'smooth' : 'instant' }) } @@ -83,5 +94,16 @@ export function useInfiniteDays(gridRef: Ref) { cancelAnimationFrame(rafId) }) - return { visibleDays, dateRange, centerDate, dayOffset, dateFromX, scrollToDate, initScroll, onScroll } + return { + visibleDays, + dateRange, + centerDate, + dayOffset, + dateFromX, + scrollToDate, + initScroll, + onScroll, + captureScrollLeft, + restoreScrollLeft, + } } diff --git a/alfy-bot-frontend/src/features/calendar/lib/week.spec.ts b/alfy-bot-frontend/src/features/calendar/lib/week.spec.ts new file mode 100644 index 0000000..06b524b --- /dev/null +++ b/alfy-bot-frontend/src/features/calendar/lib/week.spec.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest' +import { isDateInRange, pickDayViewDate } from './week' + +const local = (y: number, m: number, d: number) => new Date(y, m - 1, d) + +describe('pickDayViewDate', () => { + const range = { + start: local(2026, 8, 10), + end: local(2026, 8, 16), + } + + it('берёт сегодня, если сегодня в видимом диапазоне недели', () => { + expect(pickDayViewDate(range, local(2026, 8, 14))).toEqual(local(2026, 8, 14)) + }) + + it('не берёт середину вьюпорта — только сегодня или левый край', () => { + const picked = pickDayViewDate(range, local(2026, 8, 10)) + expect(picked).toEqual(local(2026, 8, 10)) + expect(picked).not.toEqual(local(2026, 8, 13)) + }) + + it('если сегодня вне диапазона — левый край видимой недели', () => { + expect(pickDayViewDate(range, local(2026, 8, 20))).toEqual(local(2026, 8, 10)) + }) +}) + +describe('isDateInRange', () => { + it('включает границы по календарному дню, игнорируя время', () => { + const start = new Date(2026, 7, 10, 0, 0) + const end = new Date(2026, 7, 16, 18, 30) + expect(isDateInRange(new Date(2026, 7, 10, 23, 0), start, end)).toBe(true) + expect(isDateInRange(new Date(2026, 7, 16, 9, 0), start, end)).toBe(true) + expect(isDateInRange(new Date(2026, 7, 9, 23, 0), start, end)).toBe(false) + expect(isDateInRange(new Date(2026, 7, 17, 0, 0), start, end)).toBe(false) + }) +}) diff --git a/alfy-bot-frontend/src/features/calendar/lib/week.ts b/alfy-bot-frontend/src/features/calendar/lib/week.ts index dfb9f6c..6f2988f 100644 --- a/alfy-bot-frontend/src/features/calendar/lib/week.ts +++ b/alfy-bot-frontend/src/features/calendar/lib/week.ts @@ -1,4 +1,4 @@ -import { addDays, addWeeks, eachDayOfInterval, format, isSameDay, startOfWeek } from 'date-fns' +import { addDays, addWeeks, eachDayOfInterval, format, isSameDay, startOfDay, startOfWeek } from 'date-fns' import { dateFnsLocale, weekStartsOn } from '@/composables/useLocale' export function getWeekStart(date: Date): Date { @@ -43,6 +43,21 @@ export function formatDayTitle(date: Date): string { return format(date, 'd MMMM yyyy', { locale: dateFnsLocale.value }) } +export function isDateInRange(date: Date, start: Date, end: Date): boolean { + const t = startOfDay(date).getTime() + return t >= startOfDay(start).getTime() && t <= startOfDay(end).getTime() +} + +export function pickDayViewDate( + range: { start: Date, end: Date }, + today: Date = new Date(), +): Date { + const t = startOfDay(today) + if (isDateInRange(t, range.start, range.end)) + return t + return startOfDay(range.start) +} + export function formatDateRange(start: Date, end: Date): string { const startMonth = format(start, 'LLLL', { locale: dateFnsLocale.value }) const endMonth = format(end, 'LLLL', { locale: dateFnsLocale.value }) diff --git a/alfy-bot-frontend/src/features/calendar/ui/WeeklyCalendar.vue b/alfy-bot-frontend/src/features/calendar/ui/WeeklyCalendar.vue index 2110a52..c5501ee 100644 --- a/alfy-bot-frontend/src/features/calendar/ui/WeeklyCalendar.vue +++ b/alfy-bot-frontend/src/features/calendar/ui/WeeklyCalendar.vue @@ -1,7 +1,7 @@