Agenda refactor
This commit is contained in:
parent
8d2f139e9d
commit
d3b63edefe
196
scripts/consolidate-private-calendars.ts
Normal file
196
scripts/consolidate-private-calendars.ts
Normal file
@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env ts-node
|
||||
|
||||
/**
|
||||
* Script to consolidate duplicate "Privée" calendars
|
||||
* - Identifies the calendar to keep (with active sync and most events)
|
||||
* - Migrates events from duplicate calendars to the one to keep
|
||||
* - Deletes duplicate calendars and their sync configs
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function consolidatePrivateCalendars() {
|
||||
try {
|
||||
console.log('🔍 Searching for duplicate "Privée" calendars...\n');
|
||||
|
||||
// Find all calendars named "Privée"
|
||||
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`);
|
||||
|
||||
if (privateCalendars.length <= 1) {
|
||||
console.log('✅ No duplicates found. Nothing to consolidate.\n');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the calendar to keep:
|
||||
// 1. Has active sync (syncEnabled = true)
|
||||
// 2. Has the most events
|
||||
// 3. Most recent
|
||||
const calendarsToSort = [...privateCalendars];
|
||||
calendarsToSort.sort((a, b) => {
|
||||
// Priority 1: Active sync
|
||||
const aHasActiveSync = a.syncConfig?.syncEnabled === true;
|
||||
const bHasActiveSync = b.syncConfig?.syncEnabled === true;
|
||||
if (aHasActiveSync && !bHasActiveSync) return -1;
|
||||
if (!aHasActiveSync && bHasActiveSync) return 1;
|
||||
|
||||
// Priority 2: Most events
|
||||
const aEventCount = a._count.events;
|
||||
const bEventCount = b._count.events;
|
||||
if (aEventCount !== bEventCount) {
|
||||
return bEventCount - aEventCount; // Descending
|
||||
}
|
||||
|
||||
// Priority 3: Most recent
|
||||
return b.createdAt.getTime() - a.createdAt.getTime(); // Descending
|
||||
});
|
||||
|
||||
const calendarToKeep = calendarsToSort[0];
|
||||
const calendarsToDelete = calendarsToSort.slice(1);
|
||||
|
||||
console.log('📋 Calendar to KEEP:');
|
||||
console.log(` ID: ${calendarToKeep.id}`);
|
||||
console.log(` Events: ${calendarToKeep._count.events}`);
|
||||
console.log(` Sync enabled: ${calendarToKeep.syncConfig?.syncEnabled || false}`);
|
||||
console.log(` Provider: ${calendarToKeep.syncConfig?.provider || 'none'}`);
|
||||
console.log(` Created: ${calendarToKeep.createdAt}\n`);
|
||||
|
||||
console.log(`🗑️ Calendars to DELETE (${calendarsToDelete.length}):`);
|
||||
for (const cal of calendarsToDelete) {
|
||||
console.log(` ID: ${cal.id}`);
|
||||
console.log(` Events: ${cal._count.events}`);
|
||||
console.log(` Sync enabled: ${cal.syncConfig?.syncEnabled || false}`);
|
||||
console.log(` Created: ${cal.createdAt}\n`);
|
||||
}
|
||||
|
||||
// Migrate events from calendars to delete to the calendar to keep
|
||||
let totalMigrated = 0;
|
||||
for (const calToDelete of calendarsToDelete) {
|
||||
const eventsToMigrate = await prisma.event.findMany({
|
||||
where: {
|
||||
calendarId: calToDelete.id,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`📦 Migrating ${eventsToMigrate.length} events from calendar ${calToDelete.id}...`);
|
||||
|
||||
for (const event of eventsToMigrate) {
|
||||
// Check if event with same externalEventId already exists in target calendar
|
||||
const externalId = (event as any).externalEventId;
|
||||
|
||||
if (externalId) {
|
||||
// Check if event with this externalEventId already exists in target calendar
|
||||
const existingEvent = await prisma.event.findFirst({
|
||||
where: {
|
||||
calendarId: calendarToKeep.id,
|
||||
externalEventId: externalId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingEvent) {
|
||||
console.log(` ⚠️ Skipping event "${event.title}" - already exists in target calendar (externalEventId: ${externalId.substring(0, 50)}...)`);
|
||||
// Delete the duplicate event
|
||||
await prisma.event.delete({
|
||||
where: { id: event.id },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// For events without externalEventId, check by title + date
|
||||
const existingEvent = await prisma.event.findFirst({
|
||||
where: {
|
||||
calendarId: calendarToKeep.id,
|
||||
title: event.title,
|
||||
start: event.start,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingEvent) {
|
||||
console.log(` ⚠️ Skipping event "${event.title}" - already exists in target calendar (title + date match)`);
|
||||
// Delete the duplicate event
|
||||
await prisma.event.delete({
|
||||
where: { id: event.id },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate event to target calendar
|
||||
try {
|
||||
await prisma.event.update({
|
||||
where: { id: event.id },
|
||||
data: {
|
||||
calendarId: calendarToKeep.id,
|
||||
},
|
||||
});
|
||||
totalMigrated++;
|
||||
console.log(` ✅ Migrated: "${event.title}"`);
|
||||
} catch (error) {
|
||||
console.error(` ❌ Error migrating event "${event.title}":`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Migrated ${totalMigrated} events to calendar ${calendarToKeep.id}\n`);
|
||||
|
||||
// Delete duplicate calendars and their sync configs
|
||||
for (const calToDelete of calendarsToDelete) {
|
||||
console.log(`🗑️ Deleting calendar ${calToDelete.id}...`);
|
||||
|
||||
// Delete sync config if exists
|
||||
if (calToDelete.syncConfig) {
|
||||
await prisma.calendarSync.delete({
|
||||
where: { id: calToDelete.syncConfig.id },
|
||||
});
|
||||
console.log(` ✅ Deleted sync config ${calToDelete.syncConfig.id}`);
|
||||
}
|
||||
|
||||
// Delete calendar (events are already migrated or deleted)
|
||||
await prisma.calendar.delete({
|
||||
where: { id: calToDelete.id },
|
||||
});
|
||||
console.log(` ✅ Deleted calendar ${calToDelete.id}`);
|
||||
}
|
||||
|
||||
console.log(`\n✅ Consolidation completed!`);
|
||||
console.log(` Kept: 1 calendar (${calendarToKeep.id})`);
|
||||
console.log(` Deleted: ${calendarsToDelete.length} duplicate calendars`);
|
||||
console.log(` Migrated: ${totalMigrated} events\n`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error consolidating calendars:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
consolidatePrivateCalendars()
|
||||
.then(() => {
|
||||
console.log('✅ Script completed successfully');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Script failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
150
scripts/explore-calendar-duplicates.sql
Normal file
150
scripts/explore-calendar-duplicates.sql
Normal file
@ -0,0 +1,150 @@
|
||||
-- ============================================
|
||||
-- Script d'exploration des duplications de calendriers et événements
|
||||
-- ============================================
|
||||
|
||||
-- 1. Lister tous les calendriers "Privée" avec leurs détails
|
||||
SELECT
|
||||
c.id,
|
||||
c.name,
|
||||
c.color,
|
||||
c.description,
|
||||
c."userId",
|
||||
c."missionId",
|
||||
c."createdAt",
|
||||
c."updatedAt",
|
||||
cs.id as "syncConfigId",
|
||||
cs.provider,
|
||||
cs."externalCalendarId",
|
||||
cs."syncEnabled",
|
||||
cs."lastSyncAt",
|
||||
mc.email as "mailCredentialEmail",
|
||||
(SELECT COUNT(*) FROM "Event" e WHERE e."calendarId" = c.id) as "eventCount"
|
||||
FROM "Calendar" c
|
||||
LEFT JOIN "CalendarSync" cs ON cs."calendarId" = c.id
|
||||
LEFT JOIN "MailCredentials" mc ON mc.id = cs."mailCredentialId"
|
||||
WHERE c.name = 'Privée'
|
||||
ORDER BY c."createdAt" DESC;
|
||||
|
||||
-- 2. Voir tous les événements "Vendredi saint" avec leurs calendriers
|
||||
SELECT
|
||||
e.id,
|
||||
e.title,
|
||||
e."calendarId",
|
||||
c.name as "calendarName",
|
||||
e."externalEventId",
|
||||
e.start,
|
||||
e.end,
|
||||
e."isAllDay",
|
||||
e."createdAt",
|
||||
e."updatedAt"
|
||||
FROM "Event" e
|
||||
JOIN "Calendar" c ON c.id = e."calendarId"
|
||||
WHERE e.title = 'Vendredi saint'
|
||||
ORDER BY e.start, c.name;
|
||||
|
||||
-- 3. Identifier les événements en double par externalEventId (même événement dans plusieurs calendriers)
|
||||
SELECT
|
||||
e."externalEventId",
|
||||
e.title,
|
||||
COUNT(*) as "occurrenceCount",
|
||||
STRING_AGG(DISTINCT c.id::text, ', ') as "calendarIds",
|
||||
STRING_AGG(DISTINCT c.name, ', ') as "calendarNames",
|
||||
MIN(e.start) as "earliestStart",
|
||||
MAX(e.start) as "latestStart"
|
||||
FROM "Event" e
|
||||
JOIN "Calendar" c ON c.id = e."calendarId"
|
||||
WHERE e."externalEventId" IS NOT NULL
|
||||
AND e."externalEventId" != ''
|
||||
GROUP BY e."externalEventId", e.title
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY "occurrenceCount" DESC, e.title;
|
||||
|
||||
-- 4. Voir les détails complets des événements en double
|
||||
SELECT
|
||||
e.id,
|
||||
e.title,
|
||||
e."externalEventId",
|
||||
e."calendarId",
|
||||
c.name as "calendarName",
|
||||
c."userId",
|
||||
e.start,
|
||||
e.end,
|
||||
e."isAllDay",
|
||||
e."createdAt",
|
||||
e."updatedAt"
|
||||
FROM "Event" e
|
||||
JOIN "Calendar" c ON c.id = e."calendarId"
|
||||
WHERE e."externalEventId" IN (
|
||||
SELECT "externalEventId"
|
||||
FROM "Event"
|
||||
WHERE "externalEventId" IS NOT NULL
|
||||
AND "externalEventId" != ''
|
||||
GROUP BY "externalEventId"
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
ORDER BY e."externalEventId", e."calendarId", e.start;
|
||||
|
||||
-- 5. Compter les événements par calendrier "Privée"
|
||||
SELECT
|
||||
c.id as "calendarId",
|
||||
c.name,
|
||||
COUNT(e.id) as "totalEvents",
|
||||
COUNT(CASE WHEN e."externalEventId" IS NOT NULL THEN 1 END) as "eventsWithExternalId",
|
||||
COUNT(CASE WHEN e."externalEventId" IS NULL THEN 1 END) as "eventsWithoutExternalId"
|
||||
FROM "Calendar" c
|
||||
LEFT JOIN "Event" e ON e."calendarId" = c.id
|
||||
WHERE c.name = 'Privée'
|
||||
GROUP BY c.id, c.name
|
||||
ORDER BY "totalEvents" DESC;
|
||||
|
||||
-- 6. Voir les événements "TEST de connaissances" (celui qui apparaît en double dans les logs)
|
||||
SELECT
|
||||
e.id,
|
||||
e.title,
|
||||
e."externalEventId",
|
||||
e."calendarId",
|
||||
c.name as "calendarName",
|
||||
e.start,
|
||||
e.end,
|
||||
e."createdAt"
|
||||
FROM "Event" e
|
||||
JOIN "Calendar" c ON c.id = e."calendarId"
|
||||
WHERE e.title LIKE '%TEST de connaissances%'
|
||||
OR e."externalEventId" = 'AAMkADg2NjBjMDUwLTJlMzYtNDBlZi1iNzE4LTA5YWFiYTdhMWJmYwBGAAAAAABiHJmWOydNSY0OtAePspbBBwBWDBinTymARZbnqCRYQq7IAAAAAAENAABWDBinTymARZbnqCRYQq7IAAMGtqjeAAA='
|
||||
ORDER BY e."calendarId", e.start;
|
||||
|
||||
-- 7. Lister tous les CalendarSync pour les calendriers "Privée"
|
||||
SELECT
|
||||
cs.id,
|
||||
cs."calendarId",
|
||||
c.name as "calendarName",
|
||||
cs.provider,
|
||||
cs."externalCalendarId",
|
||||
cs."syncEnabled",
|
||||
cs."lastSyncAt",
|
||||
cs."syncFrequency",
|
||||
cs."lastSyncError",
|
||||
mc.email as "mailCredentialEmail"
|
||||
FROM "CalendarSync" cs
|
||||
JOIN "Calendar" c ON c.id = cs."calendarId"
|
||||
LEFT JOIN "MailCredentials" mc ON mc.id = cs."mailCredentialId"
|
||||
WHERE c.name = 'Privée'
|
||||
ORDER BY cs."createdAt" DESC;
|
||||
|
||||
-- 8. Trouver les événements sans externalEventId dans les calendriers Microsoft
|
||||
SELECT
|
||||
e.id,
|
||||
e.title,
|
||||
e."calendarId",
|
||||
c.name as "calendarName",
|
||||
e.start,
|
||||
e.end,
|
||||
e."createdAt",
|
||||
cs.provider
|
||||
FROM "Event" e
|
||||
JOIN "Calendar" c ON c.id = e."calendarId"
|
||||
LEFT JOIN "CalendarSync" cs ON cs."calendarId" = c.id
|
||||
WHERE c.name = 'Privée'
|
||||
AND (e."externalEventId" IS NULL OR e."externalEventId" = '')
|
||||
AND cs.provider = 'microsoft'
|
||||
ORDER BY e.start DESC;
|
||||
Loading…
Reference in New Issue
Block a user