diff --git a/lib/services/microsoft-calendar-sync.ts b/lib/services/microsoft-calendar-sync.ts index dfe7e61..09a4c04 100644 --- a/lib/services/microsoft-calendar-sync.ts +++ b/lib/services/microsoft-calendar-sync.ts @@ -140,30 +140,23 @@ export async function fetchMicrosoftEvents( }; // Add date filter if provided - // 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 + // Note: Microsoft Graph API always uses dateTime for filtering, even for all-day events + // All-day events have dateTime set to midnight (00:00:00) in the event's timezone + // We filter by dateTime range which will include both timed and all-day events if (startDate && endDate) { // Format dates for Microsoft Graph API - // For dateTime filter: use ISO 8601 format + // Use ISO 8601 format for dateTime filter const startDateTimeStr = startDate.toISOString(); const endDateTimeStr = endDate.toISOString(); - // 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}')`; + // Microsoft Graph API filter: filter by start/dateTime + // This works for both timed events and all-day events (which have dateTime at midnight) + params.$filter = `start/dateTime ge '${startDateTimeStr}' and start/dateTime le '${endDateTimeStr}'`; 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) @@ -194,13 +187,35 @@ export async function fetchMicrosoftEvents( }); const events = response.data.value || []; + + // Log all event subjects to help debug missing events like "test" and "retest" + const allSubjects = events.map((e: MicrosoftEvent) => e.subject || '(sans titre)'); + const testEvents = events.filter((e: MicrosoftEvent) => + e.subject && (e.subject.toLowerCase().includes('test') || e.subject.toLowerCase().includes('retest')) + ); + logger.info('Microsoft Graph API response', { calendarId, eventCount: events.length, 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: MicrosoftEvent) => e.id), + // Log first few event IDs and subjects to verify they're being returned + eventIds: events.slice(0, 10).map((e: MicrosoftEvent) => ({ + id: e.id, + subject: e.subject || '(sans titre)', + start: e.start.dateTime || e.start.date, + isAllDay: e.isAllDay, + })), + // Log all event subjects to help debug missing events + allSubjects, + // Specifically check for test/retest events + testEventsFound: testEvents.length, + testEventDetails: testEvents.map((e: MicrosoftEvent) => ({ + id: e.id, + subject: e.subject, + start: e.start.dateTime || e.start.date, + isAllDay: e.isAllDay, + })), }); // Log if we got fewer events than expected diff --git a/scripts/cleanup-duplicate-calendars.ts b/scripts/cleanup-duplicate-calendars.ts new file mode 100644 index 0000000..59bf578 --- /dev/null +++ b/scripts/cleanup-duplicate-calendars.ts @@ -0,0 +1,122 @@ +#!/usr/bin/env ts-node + +/** + * Script to clean up duplicate calendars created from failed Microsoft account installations + * Removes calendars that have no events and no active sync config + */ + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function cleanupDuplicateCalendars() { + try { + console.log('🔍 Searching for duplicate calendars...\n'); + + // Find all calendars named "Privée" with Microsoft sync + const privateCalendars = await prisma.calendar.findMany({ + where: { + name: 'Privée', + }, + include: { + syncConfig: { + include: { + mailCredential: true, + }, + }, + _count: { + select: { + events: true, + }, + }, + }, + }); + + console.log(`Found ${privateCalendars.length} calendars named "Privée"\n`); + + // Group by email (from mailCredential) + const calendarsByEmail = new Map(); + + for (const calendar of privateCalendars) { + const email = calendar.syncConfig?.mailCredential?.email || 'unknown'; + + if (!calendarsByEmail.has(email)) { + calendarsByEmail.set(email, []); + } + calendarsByEmail.get(email)!.push(calendar); + } + + let totalDeleted = 0; + let totalKept = 0; + + for (const [email, calendars] of calendarsByEmail.entries()) { + if (calendars.length <= 1) { + continue; // No duplicates for this email + } + + console.log(`\n📧 Processing email: ${email} (${calendars.length} calendars)`); + + // Sort calendars by: + // 1. Has events (prefer calendars with events) + // 2. Has active sync (prefer calendars with active sync) + // 3. Created date (prefer newest) + calendars.sort((a, b) => { + // First: prefer calendars with events + if (a._count.events !== b._count.events) { + return b._count.events - a._count.events; + } + // Second: prefer calendars with active sync + const aHasActiveSync = a.syncConfig?.syncEnabled === true; + const bHasActiveSync = b.syncConfig?.syncEnabled === true; + if (aHasActiveSync !== bHasActiveSync) { + return bHasActiveSync ? 1 : -1; + } + // Third: prefer newest + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + }); + + const toKeep = calendars[0]; + const toDelete = calendars.slice(1); + + console.log(` ✅ Keeping: ${toKeep.id}`); + console.log(` Events: ${toKeep._count.events}`); + console.log(` Sync enabled: ${toKeep.syncConfig?.syncEnabled || false}`); + console.log(` Created: ${toKeep.createdAt.toISOString()}`); + + for (const calendar of toDelete) { + console.log(` 🗑️ Deleting: ${calendar.id}`); + console.log(` Events: ${calendar._count.events}`); + console.log(` Sync enabled: ${calendar.syncConfig?.syncEnabled || false}`); + console.log(` Created: ${calendar.createdAt.toISOString()}`); + + // Delete sync config first (if exists) + if (calendar.syncConfig) { + await prisma.calendarSync.delete({ + where: { id: calendar.syncConfig.id }, + }); + } + + // Delete calendar (events will be cascade deleted) + await prisma.calendar.delete({ + where: { id: calendar.id }, + }); + + totalDeleted++; + } + + totalKept++; + } + + console.log(`\n✅ Cleanup completed!`); + console.log(` Kept: ${totalKept} calendars`); + console.log(` Deleted: ${totalDeleted} duplicate calendars`); + + } catch (error) { + console.error('❌ Error cleaning up duplicate calendars:', error); + throw error; + } finally { + await prisma.$disconnect(); + } +} + +cleanupDuplicateCalendars();