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:
parent
6584b9c266
commit
346583fe78
@ -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": {
|
||||
|
||||
@ -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(':');
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user