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