diff --git a/alfy-bot-frontend/src/features/calendar/lib/use-create-slot-drag.ts b/alfy-bot-frontend/src/features/calendar/lib/use-create-slot-drag.ts index 43343ed..f6e3c65 100644 --- a/alfy-bot-frontend/src/features/calendar/lib/use-create-slot-drag.ts +++ b/alfy-bot-frontend/src/features/calendar/lib/use-create-slot-drag.ts @@ -1,4 +1,5 @@ -import { ref, computed, type Ref, type CSSProperties } from 'vue' +import type { CSSProperties, MaybeRefOrGetter, Ref } from 'vue' +import { computed, ref, toValue } from 'vue' const MAX_MINUTES = 23 * 60 + 55 const SHOW_OVERLAY_THRESHOLD_PX = 3 @@ -10,7 +11,7 @@ export interface CreateSlotDragOptions { trackEl: Ref dayOffset: (date: Date) => number dateFromX: (x: number) => Date - dayWidth: number + dayWidth: MaybeRefOrGetter onCreated: (date: Date, startMinutes: number, durationMinutes: number) => Promise } @@ -39,7 +40,7 @@ export function useCreateSlotDrag(options: CreateSlotDragOptions) { position: 'absolute', top: `${overlayTop.value}px`, left: `${overlayLeft.value}px`, - width: `${options.dayWidth - 4}px`, + width: `${toValue(options.dayWidth) - 4}px`, })) const overlayTimeLabel = computed(() => { @@ -49,14 +50,19 @@ export function useCreateSlotDrag(options: CreateSlotDragOptions) { }) function onPointerDown(e: PointerEvent) { - if (e.button !== 0) return - if (e.ctrlKey || e.metaKey) return + if (e.button !== 0) + return + if (e.ctrlKey || e.metaKey) + return const target = e.target as HTMLElement | null - if (!target) return - if (target.closest('[data-calendar-event-block]')) return + if (!target) + return + if (target.closest('[data-calendar-event-block]')) + return const track = options.trackEl.value - if (!track) return + if (!track) + return const rect = track.getBoundingClientRect() const initialX = e.clientX - rect.left @@ -94,7 +100,8 @@ export function useCreateSlotDrag(options: CreateSlotDragOptions) { if (!showOverlay.value) { const dx = Math.abs(ev.clientX - rect.left - initialX) const dy = Math.abs(y - initialY) - if (dx < SHOW_OVERLAY_THRESHOLD_PX && dy < SHOW_OVERLAY_THRESHOLD_PX) return + if (dx < SHOW_OVERLAY_THRESHOLD_PX && dy < SHOW_OVERLAY_THRESHOLD_PX) + return showOverlay.value = true } @@ -149,7 +156,8 @@ export function useCreateSlotDrag(options: CreateSlotDragOptions) { }, LONG_PRESS_DELAY_MS) function onWaitMove(ev: PointerEvent) { - if (timerFired) return + if (timerFired) + return const dx = ev.clientX - initialClientX const dy = ev.clientY - initialClientY if (Math.sqrt(dx * dx + dy * dy) > TOUCH_DRAG_THRESHOLD_PX) { @@ -158,7 +166,8 @@ export function useCreateSlotDrag(options: CreateSlotDragOptions) { } function onWaitEnd() { - if (!timerFired) cleanupWait() + if (!timerFired) + cleanupWait() } function cleanupWait() { diff --git a/alfy-bot-frontend/src/features/calendar/lib/use-pointer-dnd.ts b/alfy-bot-frontend/src/features/calendar/lib/use-pointer-dnd.ts index b6e87f8..9faf554 100644 --- a/alfy-bot-frontend/src/features/calendar/lib/use-pointer-dnd.ts +++ b/alfy-bot-frontend/src/features/calendar/lib/use-pointer-dnd.ts @@ -1,6 +1,7 @@ -import { ref, computed, type Ref, type CSSProperties } from 'vue' -import { startOfDay } from 'date-fns' +import type { CSSProperties, MaybeRefOrGetter, Ref } from 'vue' import type { CalendarEvent } from '../model/types' +import { startOfDay } from 'date-fns' +import { computed, ref, toValue } from 'vue' const MAX_MINUTES = 23 * 60 + 55 @@ -8,7 +9,7 @@ export interface PointerDndOptions { trackEl: Ref dayOffset: (date: Date) => number dateFromX: (x: number) => Date - dayWidth: number + dayWidth: MaybeRefOrGetter onMoved: (taskId: string, newDate: Date, startMinutes: number) => Promise onClicked?: (event: CalendarEvent) => void onResized?: (taskId: string, durationMinutes: number) => Promise @@ -39,7 +40,7 @@ export function usePointerDnd(options: PointerDndOptions) { position: 'absolute', top: `${overlayTop.value}px`, left: `${overlayLeft.value}px`, - width: `${options.dayWidth - 4}px`, + width: `${toValue(options.dayWidth) - 4}px`, })) const overlayTimeLabel = computed(() => { @@ -52,7 +53,8 @@ export function usePointerDnd(options: PointerDndOptions) { function onGrab(event: CalendarEvent, e: PointerEvent) { const track = options.trackEl.value - if (!track) return + if (!track) + return isDragging.value = true showOverlay.value = false @@ -83,7 +85,8 @@ export function usePointerDnd(options: PointerDndOptions) { previewDate.value = date overlayLeft.value = options.dayOffset(date) + 2 - if (!showOverlay.value) showOverlay.value = true + if (!showOverlay.value) + showOverlay.value = true } const cleanup = () => { @@ -106,7 +109,8 @@ export function usePointerDnd(options: PointerDndOptions) { if (movedTime || movedDay) { await options.onMoved(event.taskId, previewDate.value, previewMinutes.value) - } else if (!showOverlay.value && options.onClicked) { + } + else if (!showOverlay.value && options.onClicked) { options.onClicked(event) } @@ -125,7 +129,8 @@ export function usePointerDnd(options: PointerDndOptions) { function onResizeStart(event: CalendarEvent, e: PointerEvent) { const track = options.trackEl.value - if (!track) return + if (!track) + return isDragging.value = true showOverlay.value = false @@ -147,7 +152,8 @@ export function usePointerDnd(options: PointerDndOptions) { const newDuration = Math.max(newBottom - event.startMinutes, MIN_RESIZE_DURATION) overlayHeight.value = newDuration - if (!showOverlay.value) showOverlay.value = true + if (!showOverlay.value) + showOverlay.value = true } const cleanup = () => { diff --git a/alfy-bot-frontend/src/features/calendar/lib/week.ts b/alfy-bot-frontend/src/features/calendar/lib/week.ts index 2282ede..dfb9f6c 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 { startOfWeek, addWeeks, eachDayOfInterval, addDays, isSameDay, format } from 'date-fns' +import { addDays, addWeeks, eachDayOfInterval, format, isSameDay, startOfWeek } from 'date-fns' import { dateFnsLocale, weekStartsOn } from '@/composables/useLocale' export function getWeekStart(date: Date): Date { @@ -32,13 +32,17 @@ export function formatWeekRange(weekStart: Date): string { return `${startDay} ${startMonth} \u2013 ${endDay} ${endMonth} ${year}` } -export function formatDayHeader(date: Date): { dayName: string; dayNumber: string } { +export function formatDayHeader(date: Date): { dayName: string, dayNumber: string } { return { dayName: format(date, 'EEEEEE', { locale: dateFnsLocale.value }), dayNumber: format(date, 'd'), } } +export function formatDayTitle(date: Date): string { + return format(date, 'd MMMM yyyy', { locale: dateFnsLocale.value }) +} + 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/model/types.ts b/alfy-bot-frontend/src/features/calendar/model/types.ts index 4f3db3d..ca5fd50 100644 --- a/alfy-bot-frontend/src/features/calendar/model/types.ts +++ b/alfy-bot-frontend/src/features/calendar/model/types.ts @@ -1,4 +1,4 @@ -import type { Task, Priority } from '@/features/tasks/model/types' +import type { Priority, Task } from '@/features/tasks/model/types' export interface CalendarEvent { taskId: string @@ -16,6 +16,8 @@ export interface CalendarEvent { pomodoroLabel?: string } +export type CalendarViewMode = 'week' | 'day' + export interface CalendarDropPayload { taskId: string newDate: Date diff --git a/alfy-bot-frontend/src/features/calendar/ui/CalendarHeader.vue b/alfy-bot-frontend/src/features/calendar/ui/CalendarHeader.vue index a9ea4dc..dbbcf36 100644 --- a/alfy-bot-frontend/src/features/calendar/ui/CalendarHeader.vue +++ b/alfy-bot-frontend/src/features/calendar/ui/CalendarHeader.vue @@ -1,3 +1,29 @@ + + - - diff --git a/alfy-bot-frontend/src/features/calendar/ui/HourGrid.vue b/alfy-bot-frontend/src/features/calendar/ui/HourGrid.vue index fb5b6b0..240aa8e 100644 --- a/alfy-bot-frontend/src/features/calendar/ui/HourGrid.vue +++ b/alfy-bot-frontend/src/features/calendar/ui/HourGrid.vue @@ -132,7 +132,7 @@ const { showOverlay, draggedTaskId, draggedEvent, overlayStyle, overlayHeight, o trackEl: trackRef, dayOffset: (d: Date) => props.dayOffset(d), dateFromX: (x: number) => props.dateFromX(x), - dayWidth: props.dayWidth, + dayWidth: () => props.dayWidth, onMoved: (taskId, newDate, startMinutes) => props.onTaskMoved(taskId, newDate, startMinutes), onClicked: (event) => emit('open', event), onResized: (taskId, durationMinutes) => props.onTaskResized(taskId, durationMinutes), @@ -148,7 +148,7 @@ const { trackEl: trackRef, dayOffset: (d: Date) => props.dayOffset(d), dateFromX: (x: number) => props.dateFromX(x), - dayWidth: props.dayWidth, + dayWidth: () => props.dayWidth, onCreated: (date, startMinutes, durationMinutes) => props.onSlotCreate(date, startMinutes, durationMinutes), }) diff --git a/alfy-bot-frontend/src/features/calendar/ui/WeeklyCalendar.vue b/alfy-bot-frontend/src/features/calendar/ui/WeeklyCalendar.vue index 9e82ff6..2110a52 100644 --- a/alfy-bot-frontend/src/features/calendar/ui/WeeklyCalendar.vue +++ b/alfy-bot-frontend/src/features/calendar/ui/WeeklyCalendar.vue @@ -1,102 +1,23 @@ - - + + diff --git a/alfy-bot-frontend/tests/features/calendar/ui/CalendarHeader.spec.ts b/alfy-bot-frontend/tests/features/calendar/ui/CalendarHeader.spec.ts new file mode 100644 index 0000000..1bab808 --- /dev/null +++ b/alfy-bot-frontend/tests/features/calendar/ui/CalendarHeader.spec.ts @@ -0,0 +1,37 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import CalendarHeader from '@/features/calendar/ui/CalendarHeader.vue' + +describe('calendarHeader', () => { + it('рендерит табы Неделя и День', () => { + const wrapper = mount(CalendarHeader, { + props: { label: '10–16 марта 2026', viewMode: 'week' }, + }) + const tabs = wrapper.findAll('[aria-label="Неделя"], [aria-label="День"]') + expect(tabs).toHaveLength(2) + }) + + it('подсвечивает активный вид', () => { + const wrapper = mount(CalendarHeader, { + props: { label: '14 марта 2026', viewMode: 'day' }, + }) + expect(wrapper.get('[aria-label="День"]').attributes('aria-pressed')).toBe('true') + expect(wrapper.get('[aria-label="Неделя"]').attributes('aria-pressed')).toBe('false') + }) + + it('эмитит update:viewMode при клике на День', async () => { + const wrapper = mount(CalendarHeader, { + props: { label: '10–16 марта 2026', viewMode: 'week' }, + }) + await wrapper.get('[aria-label="День"]').trigger('click') + expect(wrapper.emitted('update:viewMode')?.[0]).toEqual(['day']) + }) + + it('не эмитит при клике на уже активный таб', async () => { + const wrapper = mount(CalendarHeader, { + props: { label: '10–16 марта 2026', viewMode: 'week' }, + }) + await wrapper.get('[aria-label="Неделя"]').trigger('click') + expect(wrapper.emitted('update:viewMode')).toBeUndefined() + }) +})