diff --git a/package.json b/package.json index f55d526..c85ce72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.71.3", + "version": "1.72.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/app/globals.css b/src/app/globals.css index 901c5f3..f676b5e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2942,6 +2942,11 @@ h3 { background: transparent; } +.time-slot.slot-drag-selected { + background: rgba(66, 133, 244, 0.15); + border-left: 3px solid rgba(66, 133, 244, 0.6); +} + .time-slot.drop-target:hover { /* Subtle drop indicator */ background: rgba(0, 154, 154, 0.04); diff --git a/src/components/CalendarEventModal.tsx b/src/components/CalendarEventModal.tsx index 9c044b0..ac959d4 100644 --- a/src/components/CalendarEventModal.tsx +++ b/src/components/CalendarEventModal.tsx @@ -39,6 +39,7 @@ interface CalendarEventModalProps { event?: any; initialDate?: Date; initialStartTime?: string; + initialEndTime?: string; connections: any[]; weekStartDay?: number; // 0=Sunday, 1=Monday language?: string; @@ -51,6 +52,7 @@ export default function CalendarEventModal({ event, initialDate, initialStartTime, + initialEndTime, connections, weekStartDay = 0, language = 'en', @@ -126,6 +128,12 @@ export default function CalendarEventModal({ const getInitialEnd = () => { if (event?.end?.dateTime) return new Date(event.end.dateTime); if (event?.endTime) return new Date(event.endTime); + if (initialEndTime && initialDate) { + const d = new Date(initialDate); + const [h, m] = initialEndTime.split(':').map(Number); + d.setHours(h, m, 0, 0); + return d; + } const start = getInitialStart(); return new Date(start.getTime() + 60 * 60 * 1000); }; diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 4cb4dce..79ebea1 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -2220,8 +2220,24 @@ export default function WeeklyView() { event?: CalendarEvent; initialDate?: Date; initialStartTime?: string; + initialEndTime?: string; }>({ isOpen: false }); + // Slot drag-to-create calendar event state + const slotDragJustEndedRef = useRef(false); + const slotDragRef = useRef<{ + active: boolean; + date: Date; + startSlot: string; + currentSlot: string; + startY: number; + } | null>(null); + const [slotDragSelection, setSlotDragSelection] = useState<{ + dateStr: string; + startSlot: string; + endSlot: string; + } | null>(null); + // Calendar event resize/drag state const [eventDragState, setEventDragState] = useState<{ eventId: string; @@ -3873,6 +3889,77 @@ export default function WeeklyView() { }; }, [eventDragState, effectiveCellDuration, calendarEvents]); + // Slot drag-to-create: mousemove + mouseup on document (always active, ref-gated) + const effectiveCellDurationRef = useRef(effectiveCellDuration); + effectiveCellDurationRef.current = effectiveCellDuration; + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + const drag = slotDragRef.current; + if (!drag) return; + // Require minimum movement to distinguish from click + if (!drag.active && Math.abs(e.clientY - drag.startY) < 5) return; + if (!drag.active) { + drag.active = true; + document.body.style.userSelect = 'none'; + } + + // Find which slot the mouse is over + const el = document.elementFromPoint(e.clientX, e.clientY); + const slotEl = el?.closest('[data-slot]') as HTMLElement | null; + if (slotEl) { + const slot = slotEl.getAttribute('data-slot'); + if (slot) { + drag.currentSlot = slot; + const dateStr = `${drag.date.getFullYear()}-${String(drag.date.getMonth() + 1).padStart(2, '0')}-${String(drag.date.getDate()).padStart(2, '0')}`; + // Determine visual range (start <= end) + const startSlot = drag.startSlot <= slot ? drag.startSlot : slot; + const endSlot = drag.startSlot <= slot ? slot : drag.startSlot; + setSlotDragSelection({ dateStr, startSlot, endSlot }); + } + } + }; + + const handleMouseUp = () => { + const drag = slotDragRef.current; + slotDragRef.current = null; + if (!drag || !drag.active) { + setSlotDragSelection(null); + document.body.style.userSelect = ''; + return; + } + setSlotDragSelection(null); + document.body.style.userSelect = ''; + // Suppress the click event that follows mouseup + slotDragJustEndedRef.current = true; + setTimeout(() => { slotDragJustEndedRef.current = false; }, 300); + + // Calculate start and end times + const startSlot = drag.startSlot <= drag.currentSlot ? drag.startSlot : drag.currentSlot; + const endSlot = drag.startSlot <= drag.currentSlot ? drag.currentSlot : drag.startSlot; + + // End time = endSlot + cellDuration + const [eh, em] = endSlot.split(':').map(Number); + const endMinutes = eh * 60 + em + effectiveCellDurationRef.current; + const endH = Math.floor(endMinutes / 60); + const endM = endMinutes % 60; + const endTime = `${String(endH).padStart(2, '0')}:${String(endM).padStart(2, '0')}`; + + setCalendarEventModal({ + isOpen: true, + initialDate: drag.date, + initialStartTime: startSlot, + initialEndTime: endTime, + }); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + // Handle recurring event drag confirm (this/all) const handleRecurringDragConfirm = async (editMode: 'this' | 'future' | 'all') => { if (!pendingRecurringDrag) return; @@ -3967,16 +4054,17 @@ export default function WeeklyView() { const rollOverdueTasks = useCallback( async (currentTasks: Task[]) => { const autoRolling = profile.autoRolling ?? false; - if (!autoRolling) return; const now = new Date(); const todayStr = formatDateToISO(now); const today = new Date(todayStr); + // Roll tasks that are explicitly marked as rolling (per-task flag), + // OR all incomplete overdue tasks if global autoRolling is enabled const overdue = currentTasks.filter( (t) => !t.completed && - t.isRolling && + (t.isRolling || autoRolling) && t.scheduledDate && formatDateToISO(new Date(t.scheduledDate)) < todayStr, ); @@ -4084,8 +4172,10 @@ export default function WeeklyView() { const rollingRanRef = useRef(false); useEffect(() => { if (rollingRanRef.current) return; - if (!profile.autoRolling) return; if (tasks.length === 0) return; + // Run if global autoRolling is on, OR if any task has per-task isRolling enabled + const hasRollingTasks = tasks.some((t) => t.isRolling && !t.completed); + if (!profile.autoRolling && !hasRollingTasks) return; rollingRanRef.current = true; rollOverdueTasks(tasks); }, [profile.autoRolling, tasks, rollOverdueTasks]); @@ -7683,6 +7773,7 @@ export default function WeeklyView() { const isOccupiedByAnyTask = isSlotOccupiedByTask(date, slot); const handleSlotClick = (e: React.MouseEvent) => { + if (slotDragJustEndedRef.current) return; // Suppress click after drag-to-create if (isProtected || isOccupiedByAnyTask) return; // Don't allow adding tasks to protected or occupied slots // Alt+Click to Create Calendar Event @@ -7724,16 +7815,38 @@ export default function WeeklyView() { dropPreview?.day === date.getDay() && dropPreview?.slot === slot; + const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + const isInDragSelection = slotDragSelection && + slotDragSelection.dateStr === dateStr && + slot >= slotDragSelection.startSlot && + slot <= slotDragSelection.endSlot; + return (
{ + // Only start slot drag on empty slots, left button, no modifiers + if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey) return; + if (isProtected || isOccupiedByAnyTask) return; + // Don't start drag if clicking on an event or task + const target = e.target as HTMLElement; + if (target.closest('.calendar-event-block, .grid-task-block, .task-input-slot')) return; + slotDragRef.current = { + active: false, + date: new Date(date), + startSlot: slot, + currentSlot: slot, + startY: e.clientY, + }; + }} onDragOver={(e) => !isProtected && !isOccupiedByTask && handleDragOver(e, date.getDay(), slot) @@ -9162,6 +9275,7 @@ export default function WeeklyView() { event={calendarEventModal.event} initialDate={calendarEventModal.initialDate} initialStartTime={calendarEventModal.initialStartTime} + initialEndTime={calendarEventModal.initialEndTime} connections={connections} weekStartDay={weekStartDay} language={language} diff --git a/src/lib/calendar-events.ts b/src/lib/calendar-events.ts index 77079d2..5762792 100644 --- a/src/lib/calendar-events.ts +++ b/src/lib/calendar-events.ts @@ -116,7 +116,7 @@ function toOutlookRecurrence(recurrence?: string, startDate?: Date, recurrenceEn const days = recurrenceDays && recurrenceDays.length > 0 ? recurrenceDays.map(d => dayNames[d]) : [dayNames[start.getDay()]]; - return { pattern: { type: 'weekly', interval, daysOfWeek: days }, range }; + return { pattern: { type: 'weekly', interval, daysOfWeek: days, firstDayOfWeek: 'monday' }, range }; } case 'monthly': return { pattern: { type: 'absoluteMonthly', interval, dayOfMonth: start.getDate() }, range }; @@ -896,6 +896,8 @@ export const createCalendarEvent = async ( const startDate = event.start?.dateTime ? new Date(event.start.dateTime) : new Date(); const outlookTz = event.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; + const outlookRecurrence = toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays); + console.log('[OUTLOOK] Creating event with recurrence:', JSON.stringify({ recurrence: event.recurrence, outlookRecurrence, outlookTz })); const createdEvent = await createOutlookEvent(accessToken, calendarId, { summary: event.title, description: event.description, @@ -903,7 +905,7 @@ export const createCalendarEvent = async ( end: { ...event.end, timeZone: outlookTz }, location: event.location, allDay: event.allDay, - recurrence: toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays), + recurrence: outlookRecurrence, reminders: event.reminders, busyStatus: event.busyStatus, visibility: event.visibility, diff --git a/src/lib/outlook-calendar.ts b/src/lib/outlook-calendar.ts index cd802d2..95c59c6 100644 --- a/src/lib/outlook-calendar.ts +++ b/src/lib/outlook-calendar.ts @@ -288,14 +288,7 @@ export const createEvent = async ( calendarId: string, event: any ) => { - const response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${encodeURIComponent(calendarId)}/events`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - 'Prefer': 'outlook.timezone="UTC"' - }, - body: JSON.stringify({ + const requestBody = { subject: event.summary, body: { contentType: 'HTML', @@ -324,7 +317,17 @@ export const createEvent = async ( type: 'required', })), } : {}), - }) + }; + console.log('[OUTLOOK] createEvent request body:', JSON.stringify(requestBody, null, 2)); + + const response = await fetch(`${GRAPH_ENDPOINT}/me/calendars/${encodeURIComponent(calendarId)}/events`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'Prefer': 'outlook.timezone="UTC"' + }, + body: JSON.stringify(requestBody) }); if (!response.ok) {