fix: Google recurring event timezone + Outlook DST shift + Outlook delete

- Google: add timeZone to start/end objects when recurrence is present
  (fixes "Missing time zone definition for start time" error)
- Outlook: pass user timezone instead of UTC for event create/update,
  convert UTC dateTime to local representation (fixes +1h DST shift
  on recurring instances)
- Outlook: use instance ID for single-occurrence delete instead of
  series master ID (fixes ErrorItemNotFound on delete)

v1.63.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-24 10:16:05 +01:00
parent 6584b9c266
commit 346583fe78
3 changed files with 46 additions and 17 deletions

View File

@ -1,6 +1,6 @@
{
"name": "my-weekly-todo-list",
"version": "1.63.0",
"version": "1.63.1",
"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": {

View File

@ -847,11 +847,15 @@ export const createCalendarEvent = async (
// Map to Google format
const rrule = toRRule(event.recurrence, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays);
// Google requires timeZone on start/end when recurrence is present
const timezone = event.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
const startObj = rrule ? { ...event.start, timeZone: timezone } : event.start;
const endObj = rrule ? { ...event.end, timeZone: timezone } : event.end;
const googleEvent: any = {
summary: event.title,
description: event.description,
start: event.start,
end: event.end,
start: startObj,
end: endObj,
location: event.location,
...(rrule ? { recurrence: [rrule] } : {}),
...(event.allDay ? { allDay: true } : {}),
@ -891,11 +895,12 @@ 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 createdEvent = await createOutlookEvent(accessToken, calendarId, {
summary: event.title,
description: event.description,
start: event.start,
end: event.end,
start: { ...event.start, timeZone: outlookTz },
end: { ...event.end, timeZone: outlookTz },
location: event.location,
allDay: event.allDay,
recurrence: toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays),
@ -1084,11 +1089,12 @@ export const updateCalendarEvent = async (
// Map to Google format
const rrule = toRRule(event.recurrence, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays);
const timezone = event.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
const googleEvent: any = {};
if (event.title !== undefined) googleEvent.summary = event.title;
if (event.description !== undefined) googleEvent.description = event.description;
if (event.start !== undefined) googleEvent.start = event.start;
if (event.end !== undefined) googleEvent.end = event.end;
if (event.start !== undefined) googleEvent.start = rrule ? { ...event.start, timeZone: timezone } : event.start;
if (event.end !== undefined) googleEvent.end = rrule ? { ...event.end, timeZone: timezone } : event.end;
if (event.location !== undefined) googleEvent.location = event.location;
if (rrule) googleEvent.recurrence = [rrule];
if (event.url) googleEvent.source = { url: event.url, title: event.url };
@ -1129,11 +1135,12 @@ export const updateCalendarEvent = async (
const baseEventId = eventId.includes('::') ? eventId.split('::')[0] : eventId;
const startDate = event.start?.dateTime ? new Date(event.start.dateTime) : new Date();
const outlookUpdateTz = event.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
const updatedEvent = await updateOutlookEvent(accessToken, calendarId, baseEventId, {
summary: event.title,
description: event.description,
start: event.start,
end: event.end,
start: event.start ? { ...event.start, timeZone: outlookUpdateTz } : undefined,
end: event.end ? { ...event.end, timeZone: outlookUpdateTz } : undefined,
location: event.location,
allDay: event.allDay,
recurrence: toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays),
@ -1371,10 +1378,22 @@ export const deleteCalendarEvent = async (
else throw new Error('Failed to refresh token');
}
const baseEventId = eventId.includes('::') ? eventId.split('::')[0] : eventId;
// Outlook: for simplicity, always delete the series for now
// (Outlook instance deletion requires fetching specific occurrence IDs)
await deleteOutlookEvent(accessToken, calendarId, baseEventId);
// Outlook event ID format for recurring: "seriesMasterId::instanceId"
const hasInstanceId = eventId.includes('::');
const seriesMasterId = hasInstanceId ? eventId.split('::')[0] : eventId;
const instanceId = hasInstanceId ? eventId.split('::')[1] : eventId;
if (deleteMode === 'this' && hasInstanceId) {
// Delete just this specific occurrence
await deleteOutlookEvent(accessToken, calendarId, instanceId);
} else if (deleteMode === 'all' || !hasInstanceId) {
// Delete the entire series (use series master ID)
await deleteOutlookEvent(accessToken, calendarId, seriesMasterId);
} else {
// 'future' or 'past' — Outlook doesn't support partial series delete easily
// Fall back to deleting the series
await deleteOutlookEvent(accessToken, calendarId, seriesMasterId);
}
return;
} else if (connection.provider === 'apple') {
const [email, appPassword] = connection.accessToken.split(':');

View File

@ -253,10 +253,20 @@ export const getUpcomingEvents = async (
const ensureTimeZone = (dateTimeObj: any) => {
if (!dateTimeObj) return dateTimeObj;
return {
dateTime: dateTimeObj.dateTime,
timeZone: dateTimeObj.timeZone || 'UTC'
};
const tz = dateTimeObj.timeZone || 'UTC';
let dt = dateTimeObj.dateTime;
// If dateTime is UTC (ends with Z) but we have a real timezone, convert to local representation
if (dt && typeof dt === 'string' && dt.endsWith('Z') && tz !== 'UTC') {
const d = new Date(dt);
// Format as local time in the target timezone: YYYY-MM-DDTHH:mm:ss.0000000
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
}).formatToParts(d);
const get = (type: string) => parts.find(p => p.type === type)?.value || '00';
dt = `${get('year')}-${get('month')}-${get('day')}T${get('hour')}:${get('minute')}:${get('second')}.0000000`;
}
return { dateTime: dt, timeZone: tz };
};
/**