Through API I read my outlook calendar and show it in a calendar from the FullCalendar library.
My problem occurs when reading an event with a duration, that all the events created in Outlook at the time of displaying them in my web calendar are subtracted two hours from the beginning and the end, I attach an exemplary image:
OUTLOOK EVENT
translation: Wed, 06/05/2024 from 1:30 p.m. to 3:00 p.m.
EVENT ON MY CALENDAR
translation: Start: 06/05/2024 11:30 a.m. | End 06/05/2024 13:00 p.m.
both occur on the 5th but the original event in outlook starts at 1:30 p.m. and ends at 3:00 p.m. and my calendar reads it as if it started at 11:00 a.m. and ended at 1:30 p.m., two hours apart.
In my code I put a message in the console to see how the time arrived from the reading with the API and it arrives in this format: Wed Jun 05 2024 11:30:00 GMT+0200 (Central European Summer Time)
so I suppose that from outlook it is arriving wrong, but I am surprised since where I live the GTM is the correct one.
This is the part that gets the events:
//EVENTS
async function getCalendarEvents(accessToken) {
const response = await fetch('https://graph.microsoft.com/v1.0/me/events', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
const errorText = await response.text();
console.error(`Error al obtener los eventos del calendario: ${response.status} ${response.statusText}`, errorText);
throw new Error('Error al obtener los eventos del calendario');
}
const data = await response.json();
return data.value.map(event => ({
title: event.subject,
start: event.start.dateTime,
end: event.end.dateTime,
allDay: event.isAllDay
}));
}
This is the part of the code that deals with the event information:
function renderCalendar(events) {
const calendarEl = document.getElementById('calendar');
const calendar = new FullCalendar.Calendar(calendarEl, {
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
buttonText: {
today: 'Hoy',
month: 'Mes',
week: 'Semana',
day: 'Día',
list: 'Lista'
},
initialView: 'dayGridMonth',
locale: 'es',
events: events,
eventClick: function(info) {
// Get the event properties
const event = info.event;
const title = event.title;
const start = event.start;
const end = event.end;
const allDay = event.allDay;
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const startFormatted = start.toLocaleString('es-ES', { timeZone: timeZone });
const endFormatted = end ? end.toLocaleString('es-ES', { timeZone: timeZone }) : 'Evento de todo el día';
// Create the event information string
let content = `<h2>${title}</h2>`;
if (!allDay) {
content += `<p><strong>Inicio:</strong> ${startFormatted}</p>`;
content += `<p><strong>Fin:</strong> ${endFormatted}</p>`;
} else {
content += `<p><strong>Evento de todo el día</strong></p>`;
}
// Show SweetAlert2 popup with event information
Swal.fire({
title: 'Información del Evento',
html: content,
icon: 'info'
});
console.log(info.event);
console.log(title);
console.log(start);
console.log(end);
console.log(allDay);
}
});
calendar.render();
}
Thanks a lot!
EDIT: Although @derpirscher's solution has worked perfectly, I added another way that a colleague recommended to me that also works
async function getCalendarEvents_Alt(accessToken) {
const response = await fetch('https://graph.microsoft.com/v1.0/me/events', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
const errorText = await response.text();
console.error(`Error al obtener los eventos del calendario: ${response.status} ${response.statusText}`, errorText);
throw new Error('Error al obtener los eventos del calendario');
}
const data = await response.json();
const events = data.value.map(event => {
// Convertir las fechas y horas a objetos de fecha considerando la zona horaria
const start = new Date(event.start.dateTime);
const end = new Date(event.end.dateTime);
// Ajustar la hora para reflejar la zona horaria local
const timeZoneOffset = start.getTimezoneOffset() * 60000; // Convertir minutos a milisegundos
const localStart = new Date(start.getTime() - timeZoneOffset);
const localEnd = new Date(end.getTime() - timeZoneOffset);
return {
title: event.subject,
start: localStart.toISOString(), // Convertir a cadena ISO para asegurar la representación correcta de la fecha y hora
end: localEnd.toISOString(),
allDay: event.isAllDay
};
});
return events;
}
You are neglecting the timezone information you get from the API. Your response contains timestamps in the format
{
dateTime: "2024-06-12T09:30:00.0000000",
timeZone: "UTC"
}
but in your code, when reading the API response, you only consider
...
start: event.start.dateTime,
end: event.end.dateTime,
...
and ignore the event.start.timeZone
information.
Thus, somewhere in your process you probably do something like
new Date(start)
to convert your timestring to a Date
object. But as the timestring does not contain any information about the timezone, it's considert to be in local time (ie in your case CEST, which is 2 hours ahead UTC).
Ie "2024-06-12T09:30:00.0000000"
in UTC
is the same point in time as "2024-06-12T11:30:00.0000000"
in CEST
. But when you do new Date("2024-06-12T09:30:00.0000000")
in a computer which is running in CEST, it will interpret this timestring in CEST (because it doesn't have any other information) thus, resulting in a 2 hour shift of your timestamps.
So when converting your strings to Date
s you have to consider the correct timezone, these strings are referring to. A very simple approach could be the following
Check if the Outlook API always delivers timestamps as UTC timezone. If yes, just add a Z
to your timestamp strings. This is the simplest marker for dateparsing, that the timestamp is to be considered as a UTC timestamp. Ie
new Date("2024-06-12T09:30:00.0000000")
will give you 9:30 CEST
but new Date("2024-06-12T09:30:00.0000000Z")
will give you 9:30 UTC
which is the same as 11:30 CEST
. Be aware, this solution breaks, if the returned timeZone
is something different than UTC!
If you want to be sure, to always have the correct time if the Outlook API returns also different timezones, you have to get that information into the timestamp somehow. Looking at the docs of that API, the timeZone
information is returned as Olson name (ie something like Europe/Paris
or UTC
), so to correctly parse that you need a library which can correctly handle this information (the built-in Date
class isn't too good with that, at least when parsing). So you could for instance use moment-js
(but this is considered depreacted and should not be used anymore)
import { moment } from "moment-timezone"
...
start: moment.tz(event.start.dateTime, event.start.timeZone).toISOSTring();
or a more modern library like luxon
import { DateTime } from "luxon"
...
start: DateTime.fromISO(event.start.dateTime, { zone: event.start.timeZone }).toISO();
Both of the snippets above will set start
to a string containing the respective timezone information. Ie either leaving the time portion as UTC and append a UTC timezone marker, or convert the time to local time and append the respective offset information. Thus, the string can then later be correctly parsed to the correct local time.