diff --git a/package.json b/package.json index d0785e2..07f7ce6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.68.1", + "version": "1.69.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/api/calendar/events/route.ts b/src/app/api/calendar/events/route.ts index 3c8686f..12e8ad0 100644 --- a/src/app/api/calendar/events/route.ts +++ b/src/app/api/calendar/events/route.ts @@ -98,9 +98,9 @@ export async function PATCH(request: NextRequest) { const body = await request.json(); const { calendarId, eventId, title, description, start, end, location, allDay, recurrence, recurrenceEndDate, recurrenceCount, recurrenceInterval, recurrenceDays, url, - reminders, busyStatus, visibility, attendees, attachments } = body; + reminders, busyStatus, visibility, attendees, attachments, editMode, recurringEventId } = body; - console.log('[API] Updating event:', { calendarId, eventId, title }); + console.log('[API] Updating event:', { calendarId, eventId, title, editMode }); if (!calendarId || !eventId) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); @@ -113,7 +113,18 @@ export async function PATCH(request: NextRequest) { return NextResponse.json({ error: 'Calendar connection not found' }, { status: 404 }); } - const event = await updateCalendarEvent(connection, calendarId, eventId, { + // For "all events" edit mode on recurring events, use the series master ID + let targetEventId = eventId; + if (editMode === 'all' && recurringEventId) { + // For Outlook composite IDs (seriesMasterId::instanceId), use just the seriesMasterId + if (recurringEventId.includes('::')) { + targetEventId = recurringEventId.split('::')[0]; + } else { + targetEventId = recurringEventId; + } + } + + const event = await updateCalendarEvent(connection, calendarId, targetEventId, { title, description, start, diff --git a/src/components/CalendarEventModal.tsx b/src/components/CalendarEventModal.tsx index 1b806e5..e484dfc 100644 --- a/src/components/CalendarEventModal.tsx +++ b/src/components/CalendarEventModal.tsx @@ -159,7 +159,35 @@ export default function CalendarEventModal({ } }; - const handleSubmit = async () => { + const buildSavePayload = (editMode?: string) => { + const activeReminders = reminders.filter(r => r.minutes >= 0); + return { + id: event?.id, + title, + description, + location, + url: url || undefined, + recurrence: recurrence === 'custom' ? customUnit.replace(/s$/, '') === 'day' ? 'daily' : customUnit.replace(/s$/, '') === 'week' ? 'weekly' : customUnit.replace(/s$/, '') === 'month' ? 'monthly' : 'yearly' + : recurrence !== 'none' ? recurrence : undefined, + recurrenceInterval: recurrence === 'custom' ? customInterval : undefined, + recurrenceDays: recurrence === 'custom' && customUnit === 'weeks' ? customDays : undefined, + recurrenceEndDate: recurrence !== 'none' && recurrenceEndType === 'date' && recurrenceEndDateValue ? recurrenceEndDateValue : undefined, + recurrenceCount: recurrence !== 'none' && recurrenceEndType === 'count' && recurrenceCount > 0 ? recurrenceCount : undefined, + calendarId, + allDay, + start: { dateTime: startDate.toISOString() }, + end: { dateTime: endDate.toISOString() }, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + reminders: activeReminders.length > 0 ? activeReminders : undefined, + busyStatus: busyStatus !== 'busy' ? busyStatus : undefined, + visibility: visibility !== 'default' ? visibility : undefined, + attendees: attendees.length > 0 ? attendees : undefined, + attachments: attachments.length > 0 ? attachments : undefined, + ...(editMode ? { editMode, recurringEventId: event?.recurringEventId } : {}), + }; + }; + + const handleSubmit = async (editMode?: string) => { if (!title.trim()) { setError('Title is required'); return; @@ -173,44 +201,28 @@ export default function CalendarEventModal({ return; } + // For existing recurring events, show options first + if (event?.id && event?.isRecurring && !editMode && !showRecurringEditOptions) { + setShowRecurringEditOptions(true); + return; + } + setIsSaving(true); setError(''); try { - const activeReminders = reminders.filter(r => r.minutes >= 0); - - await onSave({ - id: event?.id, - title, - description, - location, - url: url || undefined, - recurrence: recurrence === 'custom' ? customUnit.replace(/s$/, '') === 'day' ? 'daily' : customUnit.replace(/s$/, '') === 'week' ? 'weekly' : customUnit.replace(/s$/, '') === 'month' ? 'monthly' : 'yearly' - : recurrence !== 'none' ? recurrence : undefined, - recurrenceInterval: recurrence === 'custom' ? customInterval : undefined, - recurrenceDays: recurrence === 'custom' && customUnit === 'weeks' ? customDays : undefined, - recurrenceEndDate: recurrence !== 'none' && recurrenceEndType === 'date' && recurrenceEndDateValue ? recurrenceEndDateValue : undefined, - recurrenceCount: recurrence !== 'none' && recurrenceEndType === 'count' && recurrenceCount > 0 ? recurrenceCount : undefined, - calendarId, - allDay, - start: { dateTime: startDate.toISOString() }, - end: { dateTime: endDate.toISOString() }, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - reminders: activeReminders.length > 0 ? activeReminders : undefined, - busyStatus: busyStatus !== 'busy' ? busyStatus : undefined, - visibility: visibility !== 'default' ? visibility : undefined, - attendees: attendees.length > 0 ? attendees : undefined, - attachments: attachments.length > 0 ? attachments : undefined, - }); + await onSave(buildSavePayload(editMode)); onClose(); } catch (err: any) { console.error(err); setError(err.message || 'Failed to save event'); setIsSaving(false); + setShowRecurringEditOptions(false); } }; const [isDeleteConfirming, setIsDeleteConfirming] = useState(false); const [showRecurringDeleteOptions, setShowRecurringDeleteOptions] = useState(false); + const [showRecurringEditOptions, setShowRecurringEditOptions] = useState(false); const handleDelete = async (mode?: string) => { if (!event?.id || !onDelete) return; @@ -836,19 +848,52 @@ export default function CalendarEventModal({ Cancel )} - + {!showRecurringEditOptions ? ( + + ) : ( +
+ Save: + {[ + { mode: 'this', label: 'This event' }, + { mode: 'all', label: 'All events' }, + ].map(({ mode, label }) => ( + + ))} + +
+ )} diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 4ad7498..d4d2eac 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -2237,6 +2237,17 @@ export default function WeeklyView() { hasMoved?: boolean; } | null>(null); + // Pending recurring event edit after drag/resize — asks "this" or "all" + const [pendingRecurringDrag, setPendingRecurringDrag] = useState<{ + eventId: string; + calendarId: string; + recurringEventId?: string; + startTime: string; + endTime: string; + originalStartTime: string; + originalEndTime: string; + } | null>(null); + // Dark Mode Persistence & Class Toggle const [mounted, setMounted] = useState(false); @@ -3805,32 +3816,46 @@ export default function WeeklyView() { ? { ...ev, startTime: eventDragState.currentStartTime, endTime: eventDragState.currentEndTime } : ev )); - // Save to server - try { - const res = await fetch("/api/calendar/events", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - calendarId: eventDragState.calendarId, - eventId: eventDragState.eventId, - start: { dateTime: eventDragState.currentStartTime }, - end: { dateTime: eventDragState.currentEndTime }, - }), + + // Check if this is a recurring event — if so, ask before saving + const draggedEvent = calendarEvents.find(e => e.id === eventDragState.eventId); + if (draggedEvent?.isRecurring) { + setPendingRecurringDrag({ + eventId: eventDragState.eventId, + calendarId: eventDragState.calendarId || '', + recurringEventId: draggedEvent.recurringEventId, + startTime: eventDragState.currentStartTime, + endTime: eventDragState.currentEndTime, + originalStartTime: eventDragState.originalStartTime, + originalEndTime: eventDragState.originalEndTime, }); - if (!res.ok) { - // Revert on failure + } else { + // Non-recurring: save immediately + try { + const res = await fetch("/api/calendar/events", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + calendarId: eventDragState.calendarId, + eventId: eventDragState.eventId, + start: { dateTime: eventDragState.currentStartTime }, + end: { dateTime: eventDragState.currentEndTime }, + }), + }); + if (!res.ok) { + setRawCalendarEvents(prev => prev.map(ev => + ev.id === eventDragState.eventId + ? { ...ev, startTime: eventDragState.originalStartTime, endTime: eventDragState.originalEndTime } + : ev + )); + } + } catch { setRawCalendarEvents(prev => prev.map(ev => ev.id === eventDragState.eventId ? { ...ev, startTime: eventDragState.originalStartTime, endTime: eventDragState.originalEndTime } : ev )); } - } catch { - setRawCalendarEvents(prev => prev.map(ev => - ev.id === eventDragState.eventId - ? { ...ev, startTime: eventDragState.originalStartTime, endTime: eventDragState.originalEndTime } - : ev - )); } } setEventDragState(null); @@ -3842,7 +3867,48 @@ export default function WeeklyView() { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; - }, [eventDragState, effectiveCellDuration]); + }, [eventDragState, effectiveCellDuration, calendarEvents]); + + // Handle recurring event drag confirm (this/all) + const handleRecurringDragConfirm = async (editMode: 'this' | 'all') => { + if (!pendingRecurringDrag) return; + const { eventId, calendarId, recurringEventId, startTime, endTime, originalStartTime, originalEndTime } = pendingRecurringDrag; + try { + const res = await fetch("/api/calendar/events", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + calendarId, + eventId, + start: { dateTime: startTime }, + end: { dateTime: endTime }, + editMode, + recurringEventId, + }), + }); + if (!res.ok) { + // Revert + setRawCalendarEvents(prev => prev.map(ev => + ev.id === eventId ? { ...ev, startTime: originalStartTime, endTime: originalEndTime } : ev + )); + } + } catch { + setRawCalendarEvents(prev => prev.map(ev => + ev.id === eventId ? { ...ev, startTime: originalStartTime, endTime: originalEndTime } : ev + )); + } + setPendingRecurringDrag(null); + }; + + const handleRecurringDragCancel = () => { + if (!pendingRecurringDrag) return; + // Revert to original times + const { eventId, originalStartTime, originalEndTime } = pendingRecurringDrag; + setRawCalendarEvents(prev => prev.map(ev => + ev.id === eventId ? { ...ev, startTime: originalStartTime, endTime: originalEndTime } : ev + )); + setPendingRecurringDrag(null); + }; // Get all-day events for a specific date const getAllDayEventsForDate = useCallback( @@ -9095,6 +9161,66 @@ export default function WeeklyView() { /> ) } + {/* Recurring event drag/resize confirmation */} + {pendingRecurringDrag && ( +
+
e.stopPropagation()} + > +
+ {language === 'de' ? 'Wiederkehrendes Ereignis bearbeiten' : 'Edit recurring event'} +
+
+ {language === 'de' ? 'Änderung anwenden auf:' : 'Apply change to:'} +
+
+ + + +
+
+
+ )} {/* Focus Mode Overlay */} { showFocusMode && (