fix: respect TZID when updating CalDAV event start/end times
When updating Apple/Synology events that have DTSTART with a TZID parameter, convert the incoming UTC dateTime to local time in that timezone before writing back. Prevents ICAL.js from storing UTC values under a local timezone, which caused +1h shifts on update. v1.63.4 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c69a7d92a8
commit
c718aa584a
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.63.3",
|
"version": "1.63.4",
|
||||||
"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": {
|
||||||
|
|||||||
@ -697,25 +697,60 @@ export const updateEvent = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if existing event uses TZID (for recurring events with DST-safe times)
|
||||||
|
const existingDtStart = vevent.getFirstProperty('dtstart');
|
||||||
|
const existingTzid = existingDtStart?.getParameter('tzid') as string | undefined;
|
||||||
|
|
||||||
if (eventData.start) {
|
if (eventData.start) {
|
||||||
if (eventData.start.date) {
|
if (eventData.start.date) {
|
||||||
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true);
|
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true);
|
||||||
event.startDate.isDate = true;
|
event.startDate.isDate = true;
|
||||||
} else if (eventData.start.dateTime) {
|
} else if (eventData.start.dateTime) {
|
||||||
|
if (existingTzid) {
|
||||||
|
// Convert UTC dateTime to local time in the TZID
|
||||||
|
const d = new Date(eventData.start.dateTime);
|
||||||
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: existingTzid, year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
|
||||||
|
}).formatToParts(d);
|
||||||
|
const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
|
||||||
|
const localTime = ICAL.Time.fromData({
|
||||||
|
year: parseInt(get('year')), month: parseInt(get('month')), day: parseInt(get('day')),
|
||||||
|
hour: parseInt(get('hour')), minute: parseInt(get('minute')), second: parseInt(get('second')),
|
||||||
|
isDate: false,
|
||||||
|
});
|
||||||
|
event.startDate = localTime;
|
||||||
|
} else {
|
||||||
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true);
|
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true);
|
||||||
event.startDate.isDate = false;
|
event.startDate.isDate = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (eventData.end) {
|
if (eventData.end) {
|
||||||
if (eventData.end.date) {
|
if (eventData.end.date) {
|
||||||
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.date), true);
|
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.date), true);
|
||||||
event.endDate.isDate = true;
|
event.endDate.isDate = true;
|
||||||
} else if (eventData.end.dateTime) {
|
} else if (eventData.end.dateTime) {
|
||||||
|
if (existingTzid) {
|
||||||
|
const d = new Date(eventData.end.dateTime);
|
||||||
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: existingTzid, year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
|
||||||
|
}).formatToParts(d);
|
||||||
|
const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
|
||||||
|
const localTime = ICAL.Time.fromData({
|
||||||
|
year: parseInt(get('year')), month: parseInt(get('month')), day: parseInt(get('day')),
|
||||||
|
hour: parseInt(get('hour')), minute: parseInt(get('minute')), second: parseInt(get('second')),
|
||||||
|
isDate: false,
|
||||||
|
});
|
||||||
|
event.endDate = localTime;
|
||||||
|
} else {
|
||||||
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true);
|
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true);
|
||||||
event.endDate.isDate = false;
|
event.endDate.isDate = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update reminders (VALARM)
|
// Update reminders (VALARM)
|
||||||
if (eventData.reminders !== undefined) {
|
if (eventData.reminders !== undefined) {
|
||||||
|
|||||||
@ -714,25 +714,51 @@ export const updateEvent = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if existing event uses TZID (for recurring events with DST-safe times)
|
||||||
|
const existingDtStart = vevent.getFirstProperty('dtstart');
|
||||||
|
const existingTzid = existingDtStart?.getParameter('tzid') as string | undefined;
|
||||||
|
|
||||||
|
const toLocalIcalTime = (isoStr: string, tzid: string) => {
|
||||||
|
const d = new Date(isoStr);
|
||||||
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: tzid, year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
|
||||||
|
}).formatToParts(d);
|
||||||
|
const get = (t: string) => parts.find(p => p.type === t)?.value || '00';
|
||||||
|
return ICAL.Time.fromData({
|
||||||
|
year: parseInt(get('year')), month: parseInt(get('month')), day: parseInt(get('day')),
|
||||||
|
hour: parseInt(get('hour')), minute: parseInt(get('minute')), second: parseInt(get('second')),
|
||||||
|
isDate: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (eventData.start) {
|
if (eventData.start) {
|
||||||
if (eventData.start.date) {
|
if (eventData.start.date) {
|
||||||
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true);
|
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.date), true);
|
||||||
event.startDate.isDate = true;
|
event.startDate.isDate = true;
|
||||||
} else if (eventData.start.dateTime) {
|
} else if (eventData.start.dateTime) {
|
||||||
|
if (existingTzid) {
|
||||||
|
event.startDate = toLocalIcalTime(eventData.start.dateTime, existingTzid);
|
||||||
|
} else {
|
||||||
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true);
|
event.startDate = ICAL.Time.fromJSDate(new Date(eventData.start.dateTime), true);
|
||||||
event.startDate.isDate = false;
|
event.startDate.isDate = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (eventData.end) {
|
if (eventData.end) {
|
||||||
if (eventData.end.date) {
|
if (eventData.end.date) {
|
||||||
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.date), true);
|
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.date), true);
|
||||||
event.endDate.isDate = true;
|
event.endDate.isDate = true;
|
||||||
} else if (eventData.end.dateTime) {
|
} else if (eventData.end.dateTime) {
|
||||||
|
if (existingTzid) {
|
||||||
|
event.endDate = toLocalIcalTime(eventData.end.dateTime, existingTzid);
|
||||||
|
} else {
|
||||||
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true);
|
event.endDate = ICAL.Time.fromJSDate(new Date(eventData.end.dateTime), true);
|
||||||
event.endDate.isDate = false;
|
event.endDate.isDate = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update reminders (VALARM)
|
// Update reminders (VALARM)
|
||||||
if (eventData.reminders !== undefined) {
|
if (eventData.reminders !== undefined) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user