Agenda refactor

This commit is contained in:
alma 2026-01-15 13:56:04 +01:00
parent 27b9d5df7d
commit 23e2e6c058
2 changed files with 75 additions and 42 deletions

View File

@ -46,12 +46,26 @@ export function Calendar() {
end: event.end,
allDay: event.isAllDay,
calendar: calendar.name,
calendarColor: calendar.color
calendarColor: calendar.color,
externalEventId: event.externalEventId || null, // Add externalEventId for deduplication
}))
);
// Deduplicate events by externalEventId (if same event appears in multiple calendars)
// Keep the first occurrence of each unique externalEventId
const seenExternalIds = new Set<string>();
const deduplicatedEvents = allEvents.filter((event: any) => {
if (event.externalEventId) {
if (seenExternalIds.has(event.externalEventId)) {
return false; // Skip duplicate
}
seenExternalIds.add(event.externalEventId);
}
return true; // Keep event (either has no externalEventId or is first occurrence)
});
// Filter for upcoming events
const upcomingEvents = allEvents
const upcomingEvents = deduplicatedEvents
.filter((event: any) => new Date(event.start) >= now)
.sort((a: any, b: any) => new Date(a.start).getTime() - new Date(b.start).getTime())
.slice(0, 7);

View File

@ -18,12 +18,14 @@ export interface MicrosoftEvent {
contentType?: string;
};
start: {
dateTime: string;
timeZone: string;
dateTime?: string; // For timed events
date?: string; // For all-day events (YYYY-MM-DD format)
timeZone?: string;
};
end: {
dateTime: string;
timeZone: string;
dateTime?: string; // For timed events
date?: string; // For all-day events (YYYY-MM-DD format)
timeZone?: string;
};
location?: {
displayName?: string;
@ -138,24 +140,30 @@ export async function fetchMicrosoftEvents(
};
// Add date filter if provided
// Note: Microsoft Graph API filter syntax is limited
// We can't easily filter both dateTime and date in one query
// So we'll filter by dateTime and handle all-day events separately if needed
// Microsoft Graph API supports filtering by start/dateTime for timed events
// For all-day events, we need to filter by start/date (date only, no time)
// We'll use a combined filter that handles both cases
if (startDate && endDate) {
// Format dates for Microsoft Graph API
// Use ISO 8601 format for dateTime filter
// For dateTime filter: use ISO 8601 format
const startDateTimeStr = startDate.toISOString();
const endDateTimeStr = endDate.toISOString();
// Microsoft Graph API filter: filter by start/dateTime
// This will match timed events. All-day events might need separate handling
// but Microsoft Graph usually returns all-day events with dateTime set to start of day
params.$filter = `start/dateTime ge '${startDateTimeStr}' and start/dateTime le '${endDateTimeStr}'`;
// For date filter (all-day events): use YYYY-MM-DD format
const startDateStr = startDate.toISOString().split('T')[0];
const endDateStr = endDate.toISOString().split('T')[0];
// Combined filter: (start/dateTime >= startDate OR start/date >= startDate)
// AND (start/dateTime <= endDate OR start/date <= endDate)
// This handles both timed and all-day events
params.$filter = `(start/dateTime ge '${startDateTimeStr}' or start/date ge '${startDateStr}') and (start/dateTime le '${endDateTimeStr}' or start/date le '${endDateStr}')`;
logger.debug('Microsoft Graph API filter', {
filter: params.$filter,
startDateTime: startDateTimeStr,
endDateTime: endDateTimeStr,
startDate: startDateStr,
endDate: endDateStr,
});
} else {
// If no date filter, get all events (might be too many, but useful for debugging)
@ -192,7 +200,7 @@ export async function fetchMicrosoftEvents(
hasValue: !!response.data.value,
status: response.status,
// Log first few event IDs to verify they're being returned
eventIds: events.slice(0, 5).map(e => e.id),
eventIds: events.slice(0, 5).map((e: MicrosoftEvent) => e.id),
});
// Log if we got fewer events than expected
@ -243,23 +251,30 @@ export function convertMicrosoftEventToCalDAV(microsoftEvent: MicrosoftEvent): {
allDay: boolean;
} {
// Microsoft Graph API uses different formats for all-day vs timed events
// All-day events have dateTime in format "YYYY-MM-DD" without time
// Timed events have dateTime in ISO format with time
const isAllDay = microsoftEvent.isAllDay ||
(microsoftEvent.start.dateTime && !microsoftEvent.start.dateTime.includes('T'));
// All-day events have 'date' field (YYYY-MM-DD format) or isAllDay=true
// Timed events have 'dateTime' field (ISO format with time)
const isAllDay = microsoftEvent.isAllDay || !!microsoftEvent.start.date;
let startDate: Date;
let endDate: Date;
if (isAllDay) {
// For all-day events, parse date only (YYYY-MM-DD)
startDate = new Date(microsoftEvent.start.dateTime.split('T')[0]);
endDate = new Date(microsoftEvent.end.dateTime.split('T')[0]);
// For all-day events, use 'date' field or parse dateTime if date not available
const startDateStr = microsoftEvent.start.date || microsoftEvent.start.dateTime?.split('T')[0] || '';
const endDateStr = microsoftEvent.end.date || microsoftEvent.end.dateTime?.split('T')[0] || '';
startDate = new Date(startDateStr);
endDate = new Date(endDateStr);
// Set to start of day
startDate.setHours(0, 0, 0, 0);
endDate.setHours(0, 0, 0, 0);
} else {
// For timed events, parse full ISO datetime
if (!microsoftEvent.start.dateTime) {
throw new Error(`Timed event missing dateTime: ${JSON.stringify(microsoftEvent.start)}`);
}
if (!microsoftEvent.end.dateTime) {
throw new Error(`Timed event missing end dateTime: ${JSON.stringify(microsoftEvent.end)}`);
}
startDate = new Date(microsoftEvent.start.dateTime);
endDate = new Date(microsoftEvent.end.dateTime);
}
@ -351,7 +366,7 @@ export async function syncMicrosoftCalendar(
start: startDate.toISOString(),
end: endDate.toISOString()
},
allEvents: microsoftEvents.map(e => ({
allEvents: microsoftEvents.map((e: MicrosoftEvent) => ({
id: e.id,
subject: e.subject || '(sans titre)',
start: e.start.dateTime || e.start.date,
@ -363,25 +378,27 @@ export async function syncMicrosoftCalendar(
})),
});
// Check if "Test" event is in the list
const testEvent = microsoftEvents.find(e =>
e.subject && e.subject.toLowerCase().includes('test')
// Check if "test" or "retest" events are in the list
const testEvents = microsoftEvents.filter((e: MicrosoftEvent) =>
e.subject && (e.subject.toLowerCase().includes('test') || e.subject.toLowerCase().includes('retest'))
);
if (testEvent) {
logger.info('Found "Test" event in Microsoft response', {
if (testEvents.length > 0) {
logger.info('Found "test"/"retest" events in Microsoft response', {
calendarSyncId,
testEvent: {
id: testEvent.id,
subject: testEvent.subject,
start: testEvent.start.dateTime || testEvent.start.date,
isAllDay: testEvent.isAllDay,
}
count: testEvents.length,
events: testEvents.map((e: MicrosoftEvent) => ({
id: e.id,
subject: e.subject,
start: e.start.dateTime || e.start.date,
isAllDay: e.isAllDay,
}))
});
} else {
logger.warn('"Test" event NOT found in Microsoft response', {
logger.warn('"test"/"retest" events NOT found in Microsoft response', {
calendarSyncId,
totalEvents: microsoftEvents.length,
eventSubjects: microsoftEvents.map(e => e.subject || '(sans titre)'),
dateRange: { start: startDate.toISOString(), end: endDate.toISOString() },
eventSubjects: microsoftEvents.map((e: MicrosoftEvent) => e.subject || '(sans titre)').slice(0, 10), // First 10 for debugging
});
}
@ -395,15 +412,16 @@ export async function syncMicrosoftCalendar(
} else {
// Log events in the future to help debug
const now = new Date();
const futureEvents = microsoftEvents.filter(e => {
const futureEvents = microsoftEvents.filter((e: MicrosoftEvent) => {
const eventStart = e.start.dateTime || e.start.date;
if (!eventStart) return false;
return new Date(eventStart) > now;
});
logger.info('Microsoft events in the future', {
calendarSyncId,
futureEventCount: futureEvents.length,
totalEvents: microsoftEvents.length,
futureEvents: futureEvents.map(e => ({
futureEvents: futureEvents.map((e: MicrosoftEvent) => ({
id: e.id,
subject: e.subject || '(sans titre)',
start: e.start.dateTime || e.start.date,
@ -412,15 +430,16 @@ export async function syncMicrosoftCalendar(
});
// Also log events in the past to see all events
const pastEvents = microsoftEvents.filter(e => {
const pastEvents = microsoftEvents.filter((e: MicrosoftEvent) => {
const eventStart = e.start.dateTime || e.start.date;
if (!eventStart) return false;
return new Date(eventStart) <= now;
});
if (pastEvents.length > 0) {
logger.info('Microsoft events in the past', {
calendarSyncId,
pastEventCount: pastEvents.length,
pastEvents: pastEvents.map(e => ({
pastEvents: pastEvents.map((e: MicrosoftEvent) => ({
id: e.id,
subject: e.subject || '(sans titre)',
start: e.start.dateTime || e.start.date,
@ -487,7 +506,7 @@ export async function syncMicrosoftCalendar(
// Priority 2: Fallback to checking description for [MS_ID:xxx] (backward compatibility)
if (!existingEvent && microsoftId) {
existingEvent = existingEvents.find((e) => {
existingEvent = existingEvents.find((e: typeof existingEvents[0]) => {
// Access externalEventId safely (may not be in Prisma type if client not regenerated)
const hasExternalId = !!(e as any).externalEventId;
if (!hasExternalId && e.description && e.description.includes(`[MS_ID:${microsoftId}]`)) {
@ -510,7 +529,7 @@ export async function syncMicrosoftCalendar(
// This helps migrate old events that were created before externalEventId was added
if (!existingEvent && microsoftId) {
existingEvent = existingEvents.find(
(e) => {
(e: typeof existingEvents[0]) => {
// Access externalEventId safely (may not be in Prisma type if client not regenerated)
const hasExternalId = !!(e as any).externalEventId;
// Only match events that don't have externalEventId yet (to avoid false matches)