From 27b9d5df7d3c7761dbc17f46a1c71c1f0744211d Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 15 Jan 2026 13:37:05 +0100 Subject: [PATCH] Agenda refactor --- scripts/cleanup-duplicate-events.sql | 57 +++++++++++++++++ scripts/cleanup-duplicate-events.ts | 94 +++++++++++++++++++++++++++- 2 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 scripts/cleanup-duplicate-events.sql diff --git a/scripts/cleanup-duplicate-events.sql b/scripts/cleanup-duplicate-events.sql new file mode 100644 index 0000000..db7ec77 --- /dev/null +++ b/scripts/cleanup-duplicate-events.sql @@ -0,0 +1,57 @@ +-- Script SQL to clean up duplicate events +-- Keeps events with externalEventId, removes duplicates without externalEventId +-- For events with same title, calendarId, and date (for all-day events) or same start/end (for timed events) + +-- First, let's see the duplicates +SELECT + "calendarId", + title, + DATE("start") as start_date, + DATE("end") as end_date, + COUNT(*) as duplicate_count, + COUNT(CASE WHEN "externalEventId" IS NOT NULL THEN 1 END) as with_external_id, + COUNT(CASE WHEN "externalEventId" IS NULL THEN 1 END) as without_external_id +FROM "Event" +WHERE title IN ('Vendredi saint', 'Dimanche de Pâques') +GROUP BY "calendarId", title, DATE("start"), DATE("end") +HAVING COUNT(*) > 1 +ORDER BY title, start_date; + +-- Delete duplicates: keep the one with externalEventId, or the oldest one if none have externalEventId +WITH duplicates AS ( + SELECT + id, + "calendarId", + title, + "start", + "end", + "externalEventId", + ROW_NUMBER() OVER ( + PARTITION BY + "calendarId", + title, + DATE("start"), + DATE("end") + ORDER BY + CASE WHEN "externalEventId" IS NOT NULL THEN 0 ELSE 1 END, -- Prefer events with externalEventId + "createdAt" ASC -- If no externalEventId, keep the oldest + ) as rn + FROM "Event" + WHERE title IN ('Vendredi saint', 'Dimanche de Pâques') +) +DELETE FROM "Event" +WHERE id IN ( + SELECT id FROM duplicates WHERE rn > 1 +); + +-- Verify cleanup +SELECT + id, + title, + "calendarId", + "externalEventId", + "start", + "end" +FROM "Event" +WHERE title IN ('Vendredi saint', 'Dimanche de Pâques') +ORDER BY title, "start"; diff --git a/scripts/cleanup-duplicate-events.ts b/scripts/cleanup-duplicate-events.ts index 4474760..25f028c 100644 --- a/scripts/cleanup-duplicate-events.ts +++ b/scripts/cleanup-duplicate-events.ts @@ -33,6 +33,7 @@ async function cleanupDuplicateEvents() { end: true, calendarId: true, externalEventId: true, + isAllDay: true, }, orderBy: [ { calendarId: 'asc' }, @@ -42,10 +43,49 @@ async function cleanupDuplicateEvents() { }); // Group events by title + start + end + calendarId + // For all-day events, compare by date only (YYYY-MM-DD), not exact time const eventGroups = new Map(); for (const event of allEvents) { - const key = `${event.calendarId}|${event.title}|${event.start.toISOString()}|${event.end.toISOString()}`; + // Normalize dates to compare: for all-day events, use date only + // For timed events, round to nearest minute to handle small time differences + const startDate = new Date(event.start); + const endDate = new Date(event.end); + + // Check if it's an all-day event (use isAllDay field or detect by duration) + const duration = endDate.getTime() - startDate.getTime(); + const isAllDay = event.isAllDay || + (duration >= 23 * 60 * 60 * 1000 && duration <= 25 * 60 * 60 * 1000); // 23-25 hours + + let startKey: string; + let endKey: string; + + if (isAllDay) { + // For all-day events, compare by date only (YYYY-MM-DD) + // Use UTC date to avoid timezone issues + const startUTC = new Date(Date.UTC( + startDate.getUTCFullYear(), + startDate.getUTCMonth(), + startDate.getUTCDate() + )); + const endUTC = new Date(Date.UTC( + endDate.getUTCFullYear(), + endDate.getUTCMonth(), + endDate.getUTCDate() + )); + startKey = `${startUTC.getUTCFullYear()}-${String(startUTC.getUTCMonth() + 1).padStart(2, '0')}-${String(startUTC.getUTCDate()).padStart(2, '0')}`; + endKey = `${endUTC.getUTCFullYear()}-${String(endUTC.getUTCMonth() + 1).padStart(2, '0')}-${String(endUTC.getUTCDate()).padStart(2, '0')}`; + } else { + // For timed events, round to nearest minute to handle small differences + const startRounded = new Date(startDate); + startRounded.setSeconds(0, 0); + const endRounded = new Date(endDate); + endRounded.setSeconds(0, 0); + startKey = startRounded.toISOString(); + endKey = endRounded.toISOString(); + } + + const key = `${event.calendarId}|${event.title}|${startKey}|${endKey}`; if (!eventGroups.has(key)) { eventGroups.set(key, { @@ -73,8 +113,58 @@ async function cleanupDuplicateEvents() { console.log(`Found ${duplicates.length} groups of duplicate events\n`); + // Debug: Show all events for specific titles to understand grouping + const debugTitles = ['Vendredi saint', 'Dimanche de Pâques']; + for (const title of debugTitles) { + const titleEvents = allEvents.filter(e => e.title === title); + if (titleEvents.length > 0) { + console.log(`\n🔍 Debug: Events for "${title}":`); + for (const evt of titleEvents) { + const startDate = new Date(evt.start); + const endDate = new Date(evt.end); + const duration = endDate.getTime() - startDate.getTime(); + const isAllDay = evt.isAllDay || (duration >= 23 * 60 * 60 * 1000 && duration <= 25 * 60 * 60 * 1000); + console.log(` - ID: ${evt.id}`); + console.log(` Calendar: ${evt.calendarId}`); + console.log(` Start: ${evt.start.toISOString()} (${startDate.toLocaleDateString()})`); + console.log(` End: ${evt.end.toISOString()} (${endDate.toLocaleDateString()})`); + console.log(` Duration: ${duration / (60 * 60 * 1000)} hours`); + console.log(` isAllDay: ${evt.isAllDay}, detected: ${isAllDay}`); + console.log(` externalEventId: ${evt.externalEventId ? 'YES' : 'NO'}`); + + // Show what key would be generated + let startKey: string; + let endKey: string; + if (isAllDay) { + const startUTC = new Date(Date.UTC( + startDate.getUTCFullYear(), + startDate.getUTCMonth(), + startDate.getUTCDate() + )); + const endUTC = new Date(Date.UTC( + endDate.getUTCFullYear(), + endDate.getUTCMonth(), + endDate.getUTCDate() + )); + startKey = `${startUTC.getUTCFullYear()}-${String(startUTC.getUTCMonth() + 1).padStart(2, '0')}-${String(startUTC.getUTCDate()).padStart(2, '0')}`; + endKey = `${endUTC.getUTCFullYear()}-${String(endUTC.getUTCMonth() + 1).padStart(2, '0')}-${String(endUTC.getUTCDate()).padStart(2, '0')}`; + } else { + const startRounded = new Date(startDate); + startRounded.setSeconds(0, 0); + const endRounded = new Date(endDate); + endRounded.setSeconds(0, 0); + startKey = startRounded.toISOString(); + endKey = endRounded.toISOString(); + } + const key = `${evt.calendarId}|${evt.title}|${startKey}|${endKey}`; + console.log(` Group key: ${key}`); + } + } + } + if (duplicates.length === 0) { - console.log('✅ No duplicates found!'); + console.log('\n✅ No duplicates found!'); + console.log(' (Check debug output above to see why events are not grouped as duplicates)'); return; }