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",
|
"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",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -847,11 +847,15 @@ export const createCalendarEvent = async (
|
|||||||
|
|
||||||
// Map to Google format
|
// Map to Google format
|
||||||
const rrule = toRRule(event.recurrence, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays);
|
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 = {
|
const googleEvent: any = {
|
||||||
summary: event.title,
|
summary: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
start: event.start,
|
start: startObj,
|
||||||
end: event.end,
|
end: endObj,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
...(rrule ? { recurrence: [rrule] } : {}),
|
...(rrule ? { recurrence: [rrule] } : {}),
|
||||||
...(event.allDay ? { allDay: true } : {}),
|
...(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 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, {
|
const createdEvent = await createOutlookEvent(accessToken, calendarId, {
|
||||||
summary: event.title,
|
summary: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
start: event.start,
|
start: { ...event.start, timeZone: outlookTz },
|
||||||
end: event.end,
|
end: { ...event.end, timeZone: outlookTz },
|
||||||
location: event.location,
|
location: event.location,
|
||||||
allDay: event.allDay,
|
allDay: event.allDay,
|
||||||
recurrence: toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays),
|
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
|
// Map to Google format
|
||||||
const rrule = toRRule(event.recurrence, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays);
|
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 = {};
|
const googleEvent: any = {};
|
||||||
if (event.title !== undefined) googleEvent.summary = event.title;
|
if (event.title !== undefined) googleEvent.summary = event.title;
|
||||||
if (event.description !== undefined) googleEvent.description = event.description;
|
if (event.description !== undefined) googleEvent.description = event.description;
|
||||||
if (event.start !== undefined) googleEvent.start = event.start;
|
if (event.start !== undefined) googleEvent.start = rrule ? { ...event.start, timeZone: timezone } : event.start;
|
||||||
if (event.end !== undefined) googleEvent.end = event.end;
|
if (event.end !== undefined) googleEvent.end = rrule ? { ...event.end, timeZone: timezone } : event.end;
|
||||||
if (event.location !== undefined) googleEvent.location = event.location;
|
if (event.location !== undefined) googleEvent.location = event.location;
|
||||||
if (rrule) googleEvent.recurrence = [rrule];
|
if (rrule) googleEvent.recurrence = [rrule];
|
||||||
if (event.url) googleEvent.source = { url: event.url, title: event.url };
|
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 baseEventId = eventId.includes('::') ? eventId.split('::')[0] : eventId;
|
||||||
|
|
||||||
const startDate = event.start?.dateTime ? new Date(event.start.dateTime) : new Date();
|
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, {
|
const updatedEvent = await updateOutlookEvent(accessToken, calendarId, baseEventId, {
|
||||||
summary: event.title,
|
summary: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
start: event.start,
|
start: event.start ? { ...event.start, timeZone: outlookUpdateTz } : undefined,
|
||||||
end: event.end,
|
end: event.end ? { ...event.end, timeZone: outlookUpdateTz } : undefined,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
allDay: event.allDay,
|
allDay: event.allDay,
|
||||||
recurrence: toOutlookRecurrence(event.recurrence, startDate, event.recurrenceEndDate, event.recurrenceCount, event.recurrenceInterval, event.recurrenceDays),
|
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');
|
else throw new Error('Failed to refresh token');
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseEventId = eventId.includes('::') ? eventId.split('::')[0] : eventId;
|
// Outlook event ID format for recurring: "seriesMasterId::instanceId"
|
||||||
// Outlook: for simplicity, always delete the series for now
|
const hasInstanceId = eventId.includes('::');
|
||||||
// (Outlook instance deletion requires fetching specific occurrence IDs)
|
const seriesMasterId = hasInstanceId ? eventId.split('::')[0] : eventId;
|
||||||
await deleteOutlookEvent(accessToken, calendarId, baseEventId);
|
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;
|
return;
|
||||||
} else if (connection.provider === 'apple') {
|
} else if (connection.provider === 'apple') {
|
||||||
const [email, appPassword] = connection.accessToken.split(':');
|
const [email, appPassword] = connection.accessToken.split(':');
|
||||||
|
|||||||
@ -253,10 +253,20 @@ export const getUpcomingEvents = async (
|
|||||||
|
|
||||||
const ensureTimeZone = (dateTimeObj: any) => {
|
const ensureTimeZone = (dateTimeObj: any) => {
|
||||||
if (!dateTimeObj) return dateTimeObj;
|
if (!dateTimeObj) return dateTimeObj;
|
||||||
return {
|
const tz = dateTimeObj.timeZone || 'UTC';
|
||||||
dateTime: dateTimeObj.dateTime,
|
let dt = dateTimeObj.dateTime;
|
||||||
timeZone: dateTimeObj.timeZone || 'UTC'
|
// 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