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 && (