719 lines
26 KiB
TypeScript
719 lines
26 KiB
TypeScript
import { getServerSession } from "next-auth/next";
|
|
import { authOptions } from "@/app/api/auth/options";
|
|
import { redirect } from "next/navigation";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { CalendarClient } from "@/components/calendar/calendar-client";
|
|
import { Metadata } from "next";
|
|
import { CalendarDays, Users, Bookmark, Clock } from "lucide-react";
|
|
import Image from "next/image";
|
|
import { Button } from "@/components/ui/button";
|
|
import { add } from 'date-fns';
|
|
|
|
export const metadata: Metadata = {
|
|
title: "NEAH - Calendrier | Gestion d'événements professionnelle",
|
|
description: "Plateforme avancée pour la gestion de vos rendez-vous, réunions et événements professionnels",
|
|
keywords: "calendrier, rendez-vous, événements, gestion du temps, NEAH",
|
|
};
|
|
|
|
interface Event {
|
|
id: string;
|
|
title: string;
|
|
description?: string | null;
|
|
start: Date;
|
|
end: Date;
|
|
location?: string | null;
|
|
isAllDay: boolean;
|
|
type?: string;
|
|
attendees?: { id: string; name: string }[];
|
|
}
|
|
|
|
interface Calendar {
|
|
id: string;
|
|
name: string;
|
|
color: string;
|
|
description?: string | null;
|
|
events: Event[];
|
|
}
|
|
|
|
export default async function CalendarPage() {
|
|
const session = await getServerSession(authOptions);
|
|
|
|
if (!session?.user) {
|
|
redirect("/api/auth/signin");
|
|
}
|
|
|
|
const userId = session.user.username || session.user.email || '';
|
|
|
|
// Ensure user has a default private calendar (created automatically if missing)
|
|
// This is the user's personal calendar, distinct from Microsoft synced calendars
|
|
const defaultPrivateCalendarName = "Mon Calendrier";
|
|
const existingDefaultCalendar = await prisma.calendar.findFirst({
|
|
where: {
|
|
userId: session?.user?.id || '',
|
|
name: defaultPrivateCalendarName,
|
|
missionId: null, // Not a mission calendar
|
|
syncConfig: null, // Not a synced calendar
|
|
}
|
|
});
|
|
|
|
if (!existingDefaultCalendar) {
|
|
// Create default private calendar for the user
|
|
await prisma.calendar.create({
|
|
data: {
|
|
name: defaultPrivateCalendarName,
|
|
color: "#4f46e5", // Indigo color
|
|
description: "Votre calendrier personnel",
|
|
userId: session?.user?.id || '',
|
|
missionId: null,
|
|
}
|
|
});
|
|
console.log(`[AGENDA] Created default private calendar for user ${session?.user?.id}`);
|
|
}
|
|
|
|
// Get all calendars for the user with mission relation and sync configuration
|
|
// This includes:
|
|
// 1. Personal calendars (userId = session.user.id) - including the default private calendar
|
|
// 2. Microsoft synced calendars (named "Privée" with syncConfig)
|
|
// 3. Mission calendars where user is associated via MissionUser
|
|
// 4. Group calendars where user is a member
|
|
// Exclude "Privée" and "Default" calendars that are not synced (they should only exist if synced from courrier)
|
|
|
|
// Get personal calendars
|
|
// First, get all calendars, then filter in code for "Privée"/"Default" with sync
|
|
let personalCalendars = await prisma.calendar.findMany({
|
|
where: {
|
|
userId: session?.user?.id || '',
|
|
OR: [
|
|
// Keep calendars that are not "Privée" or "Default"
|
|
{ name: { notIn: ["Privée", "Default"] } },
|
|
// Or keep "Privée"/"Default" calendars that have sync config (we'll filter syncEnabled in code)
|
|
{
|
|
AND: [
|
|
{ name: { in: ["Privée", "Default"] } },
|
|
{
|
|
syncConfig: {
|
|
isNot: null
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
include: {
|
|
events: {
|
|
orderBy: {
|
|
start: 'asc'
|
|
}
|
|
},
|
|
mission: {
|
|
include: {
|
|
missionUsers: true
|
|
}
|
|
},
|
|
syncConfig: {
|
|
include: {
|
|
mailCredential: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
display_name: true,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Filter out "Privée"/"Default" calendars that don't have syncEnabled=true
|
|
personalCalendars = personalCalendars.filter(cal => {
|
|
// Keep all non-"Privée"/"Default" calendars
|
|
if (cal.name !== "Privée" && cal.name !== "Default") {
|
|
return true;
|
|
}
|
|
// For "Privée"/"Default", only keep if syncConfig exists and is enabled
|
|
return cal.syncConfig && cal.syncConfig.syncEnabled === true;
|
|
});
|
|
|
|
// Get mission calendars where user is associated via MissionUser
|
|
const missionUserRelations = await prisma.missionUser.findMany({
|
|
where: {
|
|
userId: session?.user?.id || '',
|
|
},
|
|
include: {
|
|
mission: {
|
|
include: {
|
|
calendars: {
|
|
where: {
|
|
// Exclude calendars already owned by user (already in personalCalendars)
|
|
userId: { not: session?.user?.id || '' }
|
|
},
|
|
include: {
|
|
events: {
|
|
orderBy: {
|
|
start: 'asc'
|
|
}
|
|
},
|
|
mission: {
|
|
include: {
|
|
missionUsers: true
|
|
}
|
|
},
|
|
syncConfig: {
|
|
include: {
|
|
mailCredential: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
display_name: true,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Extract mission calendars
|
|
const missionCalendars = missionUserRelations.flatMap(mu => mu.mission.calendars);
|
|
|
|
// Debug: Log calendar filtering
|
|
console.log('[AGENDA] Calendar filtering:', {
|
|
personalCalendarsCount: personalCalendars.length,
|
|
personalCalendars: personalCalendars.map(cal => ({
|
|
id: cal.id,
|
|
name: cal.name,
|
|
hasSyncConfig: !!cal.syncConfig,
|
|
syncEnabled: cal.syncConfig?.syncEnabled,
|
|
provider: cal.syncConfig?.provider,
|
|
})),
|
|
missionCalendarsCount: missionCalendars.length,
|
|
});
|
|
|
|
// Combine personal and mission calendars
|
|
let calendars = [...personalCalendars, ...missionCalendars];
|
|
|
|
// Auto-setup sync for email accounts from courrier (Microsoft only)
|
|
// Get all Microsoft email accounts (OAuth)
|
|
const microsoftAccounts = await prisma.mailCredentials.findMany({
|
|
where: {
|
|
userId: session?.user?.id || '',
|
|
host: {
|
|
contains: 'outlook.office365.com'
|
|
},
|
|
use_oauth: true,
|
|
refresh_token: {
|
|
not: null
|
|
}
|
|
},
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
display_name: true,
|
|
refresh_token: true,
|
|
use_oauth: true
|
|
}
|
|
});
|
|
|
|
// Clean up orphaned syncs FIRST (before creating new ones)
|
|
// This handles the case where a user deleted and re-added an email account
|
|
const allMailCredentialIds = new Set([
|
|
...microsoftAccounts.map(acc => acc.id)
|
|
]);
|
|
|
|
const orphanedSyncs = await prisma.calendarSync.findMany({
|
|
where: {
|
|
calendar: {
|
|
userId: session?.user?.id || ''
|
|
},
|
|
mailCredentialId: {
|
|
not: null
|
|
}
|
|
},
|
|
include: {
|
|
calendar: true,
|
|
mailCredential: true
|
|
}
|
|
});
|
|
|
|
// Delete syncs where mailCredential no longer exists
|
|
for (const sync of orphanedSyncs) {
|
|
if (sync.mailCredentialId && !allMailCredentialIds.has(sync.mailCredentialId)) {
|
|
console.log(`[AGENDA] Deleting orphaned sync for non-existent mailCredentialId: ${sync.mailCredentialId}`);
|
|
// Delete the calendar if it has no events
|
|
const eventCount = await prisma.event.count({
|
|
where: { calendarId: sync.calendarId }
|
|
});
|
|
if (eventCount === 0) {
|
|
await prisma.calendar.delete({
|
|
where: { id: sync.calendarId }
|
|
});
|
|
} else {
|
|
// Just disable the sync, keep the calendar
|
|
await prisma.calendarSync.update({
|
|
where: { id: sync.id },
|
|
data: { syncEnabled: false }
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// For each Microsoft account, ensure there's a synced calendar
|
|
for (const account of microsoftAccounts) {
|
|
// Check if a calendar sync already exists for this account (enabled or disabled)
|
|
// This prevents creating duplicate calendars for the same account
|
|
const existingSync = await prisma.calendarSync.findFirst({
|
|
where: {
|
|
mailCredentialId: account.id
|
|
},
|
|
include: {
|
|
calendar: true
|
|
}
|
|
});
|
|
|
|
// If sync exists but is disabled, re-enable it instead of creating a new calendar
|
|
if (existingSync && !existingSync.syncEnabled) {
|
|
await prisma.calendarSync.update({
|
|
where: { id: existingSync.id },
|
|
data: { syncEnabled: true }
|
|
});
|
|
continue; // Skip to next account
|
|
}
|
|
|
|
if (!existingSync) {
|
|
// Try to discover calendars for this account
|
|
try {
|
|
const { discoverMicrosoftCalendars } = await import('@/lib/services/microsoft-calendar-sync');
|
|
const externalCalendars = await discoverMicrosoftCalendars(
|
|
session?.user?.id || '',
|
|
account.email
|
|
);
|
|
|
|
if (externalCalendars.length > 0) {
|
|
// Use the first calendar (usually the main calendar)
|
|
const mainCalendar = externalCalendars[0];
|
|
|
|
// Create a private calendar for this account
|
|
const calendar = await prisma.calendar.create({
|
|
data: {
|
|
name: "Privée",
|
|
color: "#0078D4", // Microsoft blue
|
|
description: `Calendrier synchronisé avec ${account.display_name || account.email}`,
|
|
userId: session?.user?.id || '',
|
|
}
|
|
});
|
|
|
|
// Create sync configuration
|
|
// Use 5 minutes for Microsoft (more reactive than 15 minutes)
|
|
await prisma.calendarSync.create({
|
|
data: {
|
|
calendarId: calendar.id,
|
|
mailCredentialId: account.id,
|
|
provider: 'microsoft',
|
|
externalCalendarId: mainCalendar.id,
|
|
externalCalendarUrl: mainCalendar.webLink || mainCalendar.id,
|
|
syncEnabled: true,
|
|
syncFrequency: 5
|
|
}
|
|
});
|
|
|
|
// Trigger initial sync
|
|
try {
|
|
const { syncMicrosoftCalendar } = await import('@/lib/services/microsoft-calendar-sync');
|
|
const syncConfig = await prisma.calendarSync.findUnique({
|
|
where: { calendarId: calendar.id },
|
|
include: {
|
|
calendar: true,
|
|
mailCredential: true
|
|
}
|
|
});
|
|
if (syncConfig) {
|
|
await syncMicrosoftCalendar(syncConfig.id, true);
|
|
}
|
|
} catch (syncError) {
|
|
console.error('Error during initial Microsoft sync:', syncError);
|
|
// Don't fail if sync fails, calendar is still created
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Microsoft sync setup failed - likely because account doesn't have calendar scope yet
|
|
// This is expected for accounts authenticated before calendar scope was added
|
|
// User will need to re-authenticate their Microsoft account to get calendar access
|
|
console.log(`Microsoft calendar sync not available for ${account.email} - account may need re-authentication with calendar permissions`);
|
|
// Don't fail the page - continue with other accounts
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up duplicate calendars for the same mailCredentialId
|
|
// NOTE: This is mainly for cleaning up OLD duplicates created before we fixed the cascade deletion
|
|
// New duplicates should not occur since we now delete calendars when mail accounts are deleted
|
|
// This cleanup is conservative: only processes if there are multiple ENABLED syncs for the same account
|
|
|
|
// Only run cleanup if there are actually multiple syncs to check (performance optimization)
|
|
const syncCountCheck = await prisma.calendarSync.count({
|
|
where: {
|
|
calendar: {
|
|
userId: session?.user?.id || ''
|
|
},
|
|
mailCredentialId: {
|
|
in: Array.from(allMailCredentialIds)
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`[AGENDA] Cleanup check: ${syncCountCheck} syncs found for ${allMailCredentialIds.size} mail accounts`);
|
|
|
|
// Only run full cleanup if there are multiple syncs (likely duplicates)
|
|
if (syncCountCheck > allMailCredentialIds.size) {
|
|
console.log(`[AGENDA] Running cleanup: ${syncCountCheck} syncs > ${allMailCredentialIds.size} accounts (potential duplicates)`);
|
|
const allSyncs = await prisma.calendarSync.findMany({
|
|
where: {
|
|
calendar: {
|
|
userId: session?.user?.id || ''
|
|
},
|
|
mailCredentialId: {
|
|
in: Array.from(allMailCredentialIds)
|
|
}
|
|
},
|
|
include: {
|
|
calendar: true,
|
|
mailCredential: true
|
|
},
|
|
orderBy: {
|
|
createdAt: 'desc'
|
|
}
|
|
});
|
|
|
|
// Group by mailCredentialId and provider
|
|
const syncsByAccount = new Map<string, typeof allSyncs>();
|
|
for (const sync of allSyncs) {
|
|
if (sync.mailCredentialId) {
|
|
const key = `${sync.mailCredentialId}-${sync.provider}`;
|
|
if (!syncsByAccount.has(key)) {
|
|
syncsByAccount.set(key, []);
|
|
}
|
|
syncsByAccount.get(key)!.push(sync);
|
|
}
|
|
}
|
|
|
|
// For each account, keep only the most recent enabled sync, disable or delete others
|
|
// IMPORTANT: Only process if there are actually multiple ENABLED syncs to avoid disabling active syncs
|
|
for (const [key, syncs] of syncsByAccount.entries()) {
|
|
if (syncs.length > 1) {
|
|
// Count how many are actually enabled
|
|
const enabledSyncs = syncs.filter(s => s.syncEnabled === true);
|
|
|
|
// Only clean up if there are multiple ENABLED syncs
|
|
// If there's only one enabled sync, don't touch it even if there are disabled duplicates
|
|
if (enabledSyncs.length > 1) {
|
|
console.log(`[AGENDA] Found ${enabledSyncs.length} enabled syncs for ${key}, cleaning up duplicates`);
|
|
|
|
// Sort by syncEnabled first (enabled first), then by lastSyncAt (most recently synced first), then by createdAt (newest first)
|
|
syncs.sort((a, b) => {
|
|
if (a.syncEnabled !== b.syncEnabled) {
|
|
return a.syncEnabled ? -1 : 1;
|
|
}
|
|
// Among enabled syncs, prefer the one with most recent sync
|
|
if (a.syncEnabled && b.syncEnabled) {
|
|
if (a.lastSyncAt && b.lastSyncAt) {
|
|
return new Date(b.lastSyncAt).getTime() - new Date(a.lastSyncAt).getTime();
|
|
}
|
|
if (a.lastSyncAt && !b.lastSyncAt) return -1;
|
|
if (!a.lastSyncAt && b.lastSyncAt) return 1;
|
|
}
|
|
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
});
|
|
|
|
const keepSync = syncs[0];
|
|
const duplicates = syncs.slice(1);
|
|
|
|
// Additional safety check: only disable duplicates if the sync to keep is actually valid
|
|
// (has events or has been synced recently)
|
|
const keepSyncEventCount = await prisma.event.count({
|
|
where: { calendarId: keepSync.calendarId }
|
|
});
|
|
const keepSyncIsValid = keepSyncEventCount > 0 || keepSync.lastSyncAt;
|
|
|
|
if (!keepSyncIsValid) {
|
|
console.log(`[AGENDA] WARNING: Sync to keep ${keepSync.id} has no events and no sync history - skipping cleanup to avoid disabling valid syncs`);
|
|
continue; // Skip this group to avoid disabling a potentially valid sync
|
|
}
|
|
|
|
console.log(`[AGENDA] Keeping sync ${keepSync.id} (calendar: ${keepSync.calendar.name}, events: ${keepSyncEventCount}), disabling ${duplicates.length} duplicates`);
|
|
|
|
// Disable or delete duplicate syncs
|
|
for (const duplicate of duplicates) {
|
|
if (duplicate.syncEnabled) {
|
|
// Disable the duplicate sync
|
|
console.log(`[AGENDA] Disabling duplicate sync ${duplicate.id} (calendar: ${duplicate.calendar.name})`);
|
|
await prisma.calendarSync.update({
|
|
where: { id: duplicate.id },
|
|
data: { syncEnabled: false }
|
|
});
|
|
}
|
|
// Delete the calendar if it has no events
|
|
const eventCount = await prisma.event.count({
|
|
where: { calendarId: duplicate.calendarId }
|
|
});
|
|
if (eventCount === 0) {
|
|
console.log(`[AGENDA] Deleting empty calendar ${duplicate.calendar.name} (${duplicate.calendarId})`);
|
|
await prisma.calendar.delete({
|
|
where: { id: duplicate.calendarId }
|
|
});
|
|
}
|
|
}
|
|
} else if (enabledSyncs.length === 1 && syncs.length > 1) {
|
|
// There's only one enabled sync but multiple disabled ones - this is fine, don't touch anything
|
|
console.log(`[AGENDA] Found 1 enabled sync and ${syncs.length - 1} disabled syncs for ${key} - no cleanup needed`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Auto-sync Microsoft calendars if needed (background, don't block page load)
|
|
const microsoftSyncConfigs = await prisma.calendarSync.findMany({
|
|
where: {
|
|
provider: 'microsoft',
|
|
syncEnabled: true,
|
|
calendar: {
|
|
userId: session?.user?.id || ''
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`[AGENDA] Found ${microsoftSyncConfigs.length} Microsoft sync configs`);
|
|
|
|
// Trigger sync for Microsoft calendars that need it (async, don't wait)
|
|
for (const syncConfig of microsoftSyncConfigs) {
|
|
// For Microsoft, use a more frequent check (1 minute) for better reactivity
|
|
// This allows new events to appear faster without overloading the API
|
|
const microsoftMinSyncInterval = 1; // minutes (reduced from 2 to 1 for faster sync)
|
|
const minutesSinceLastSync = syncConfig.lastSyncAt
|
|
? (Date.now() - syncConfig.lastSyncAt.getTime()) / (1000 * 60)
|
|
: Infinity;
|
|
|
|
// Sync if never synced, or if enough time has passed (use minimum of 1 min or configured frequency)
|
|
// Also sync if last sync had an error or if calendar has no events (might be a new setup)
|
|
const calendar = await prisma.calendar.findUnique({
|
|
where: { id: syncConfig.calendarId },
|
|
include: {
|
|
_count: {
|
|
select: { events: true }
|
|
}
|
|
}
|
|
});
|
|
|
|
const hasEvents = (calendar?._count?.events || 0) > 0;
|
|
const hasError = !!syncConfig.lastSyncError;
|
|
|
|
const needsSync = !syncConfig.lastSyncAt ||
|
|
minutesSinceLastSync >= Math.min(microsoftMinSyncInterval, syncConfig.syncFrequency) ||
|
|
hasError || // Force sync if there was an error
|
|
(!hasEvents && syncConfig.lastSyncAt); // Force sync if calendar has no events but was synced before
|
|
|
|
console.log(`[AGENDA] Microsoft sync config ${syncConfig.id}: lastSyncAt=${syncConfig.lastSyncAt}, minutesSinceLastSync=${minutesSinceLastSync.toFixed(1)}, needsSync=${needsSync}, syncFrequency=${syncConfig.syncFrequency}, hasEvents=${hasEvents}, hasError=${hasError}`);
|
|
|
|
if (needsSync) {
|
|
console.log(`[AGENDA] Triggering background sync for Microsoft calendar ${syncConfig.id}`);
|
|
// Trigger sync in background (don't await to avoid blocking page load)
|
|
// The sync will update the database, and the next page load will show the events
|
|
// Use forceSync=true because we've already checked that sync is needed
|
|
import('@/lib/services/microsoft-calendar-sync').then(({ syncMicrosoftCalendar }) => {
|
|
syncMicrosoftCalendar(syncConfig.id, true).then((result) => {
|
|
console.log(`[AGENDA] Microsoft sync completed:`, {
|
|
calendarSyncId: syncConfig.id,
|
|
calendarId: syncConfig.calendarId,
|
|
synced: result.synced,
|
|
created: result.created,
|
|
updated: result.updated,
|
|
deleted: result.deleted,
|
|
});
|
|
|
|
// Verify events were created by checking the database
|
|
prisma.event.count({
|
|
where: { calendarId: syncConfig.calendarId }
|
|
}).then((count) => {
|
|
console.log(`[AGENDA] Total events in calendar ${syncConfig.calendarId} after sync: ${count}`);
|
|
}).catch((err) => {
|
|
console.error('[AGENDA] Error counting events:', err);
|
|
});
|
|
}).catch((error) => {
|
|
console.error('[AGENDA] Background sync failed for Microsoft calendar', {
|
|
calendarSyncId: syncConfig.id,
|
|
calendarId: syncConfig.calendarId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
});
|
|
});
|
|
} else {
|
|
console.log(`[AGENDA] Microsoft sync skipped - too soon since last sync (${minutesSinceLastSync.toFixed(1)} min < ${Math.min(microsoftMinSyncInterval, syncConfig.syncFrequency)} min)`);
|
|
}
|
|
}
|
|
|
|
// Refresh calendars after auto-setup and cleanup
|
|
// Exclude "Privée" and "Default" calendars that are not synced
|
|
// IMPORTANT: Include all "Privée"/"Default" calendars that have ANY syncConfig (enabled or disabled)
|
|
// We'll filter by syncEnabled later
|
|
calendars = await prisma.calendar.findMany({
|
|
where: {
|
|
userId: session?.user?.id || '',
|
|
OR: [
|
|
// Keep calendars that are not "Privée" or "Default"
|
|
{ name: { notIn: ["Privée", "Default"] } },
|
|
// Or keep "Privée"/"Default" calendars that have ANY sync config (we'll filter by enabled later)
|
|
{
|
|
AND: [
|
|
{ name: { in: ["Privée", "Default"] } },
|
|
{
|
|
syncConfig: {
|
|
isNot: null
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
include: {
|
|
events: {
|
|
orderBy: {
|
|
start: 'asc'
|
|
}
|
|
},
|
|
mission: {
|
|
include: {
|
|
missionUsers: true
|
|
}
|
|
},
|
|
syncConfig: {
|
|
include: {
|
|
mailCredential: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
display_name: true,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// No default calendar creation - only synced calendars from courrier
|
|
|
|
// Filter out "Privée" and "Default" calendars that don't have active sync
|
|
calendars = calendars.filter(cal => {
|
|
const isPrivateOrDefault = cal.name === "Privée" || cal.name === "Default";
|
|
const hasActiveSync = cal.syncConfig?.syncEnabled === true && cal.syncConfig?.mailCredential;
|
|
|
|
// Exclude "Privée"/"Default" calendars that are not actively synced
|
|
if (isPrivateOrDefault && !hasActiveSync) {
|
|
console.log(`[AGENDA] Filtering out calendar ${cal.name} (${cal.id}): syncEnabled=${cal.syncConfig?.syncEnabled}, hasMailCredential=${!!cal.syncConfig?.mailCredential}`);
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// Debug: Log all calendars with syncConfig to see what we have
|
|
const calendarsWithSync = calendars.filter(cal => cal.syncConfig);
|
|
console.log(`[AGENDA] Total calendars with syncConfig: ${calendarsWithSync.length}`);
|
|
calendarsWithSync.forEach(cal => {
|
|
const eventCount = cal.events?.length || 0;
|
|
console.log(`[AGENDA] Calendar: ${cal.name}, provider: ${cal.syncConfig?.provider}, syncEnabled: ${cal.syncConfig?.syncEnabled}, hasMailCredential: ${!!cal.syncConfig?.mailCredential}, events: ${eventCount}`);
|
|
if (cal.syncConfig?.provider === 'microsoft') {
|
|
// Log all Microsoft events with details
|
|
console.log(`[AGENDA] Microsoft calendar ${cal.id} events (${eventCount}):`, cal.events.map(e => ({
|
|
id: e.id,
|
|
title: e.title,
|
|
start: e.start,
|
|
end: e.end,
|
|
isAllDay: e.isAllDay,
|
|
description: e.description ? e.description.substring(0, 50) : null
|
|
})));
|
|
|
|
// Also check directly in DB to see if there are more events
|
|
prisma.event.count({
|
|
where: { calendarId: cal.id }
|
|
}).then((dbCount) => {
|
|
if (dbCount !== eventCount) {
|
|
console.log(`[AGENDA] WARNING: Calendar ${cal.id} has ${eventCount} events in query but ${dbCount} in DB`);
|
|
}
|
|
}).catch((err) => {
|
|
console.error('[AGENDA] Error counting events in DB:', err);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Sort calendars: "Mon Calendrier" first, then synced, then groups, then missions
|
|
calendars = calendars.sort((a, b) => {
|
|
const aIsMonCalendrier = a.name === "Mon Calendrier";
|
|
const bIsMonCalendrier = b.name === "Mon Calendrier";
|
|
const aIsSynced = a.syncConfig?.syncEnabled && a.syncConfig?.mailCredential;
|
|
const bIsSynced = b.syncConfig?.syncEnabled && b.syncConfig?.mailCredential;
|
|
const aIsGroup = a.name?.startsWith("Groupe:");
|
|
const bIsGroup = b.name?.startsWith("Groupe:");
|
|
const aIsMission = a.name?.startsWith("Mission:");
|
|
const bIsMission = b.name?.startsWith("Mission:");
|
|
|
|
// "Mon Calendrier" always first
|
|
if (aIsMonCalendrier && !bIsMonCalendrier) return -1;
|
|
if (!aIsMonCalendrier && bIsMonCalendrier) return 1;
|
|
|
|
// Synced calendars second
|
|
if (aIsSynced && !bIsSynced) return -1;
|
|
if (!aIsSynced && bIsSynced) return 1;
|
|
|
|
// Groups third
|
|
if (aIsGroup && !bIsGroup && !bIsSynced) return -1;
|
|
if (!aIsGroup && bIsGroup && !aIsSynced) return 1;
|
|
|
|
// Missions fourth
|
|
if (aIsMission && !bIsMission && !bIsGroup && !bIsSynced) return -1;
|
|
if (!aIsMission && bIsMission && !aIsGroup && !aIsSynced) return 1;
|
|
|
|
// Same type, sort by name
|
|
return (a.name || '').localeCompare(b.name || '');
|
|
});
|
|
|
|
const now = new Date();
|
|
const nextWeek = add(now, { days: 7 });
|
|
|
|
const upcomingEvents = calendars.flatMap(cal =>
|
|
cal.events.filter(event =>
|
|
new Date(event.start) >= now &&
|
|
new Date(event.start) <= nextWeek
|
|
)
|
|
).sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
|
|
|
// Calculate statistics
|
|
const totalEvents = calendars.flatMap(cal => cal.events).length;
|
|
|
|
const totalMeetingHours = calendars
|
|
.flatMap(cal => cal.events)
|
|
.reduce((total, event) => {
|
|
const start = new Date(event.start);
|
|
const end = new Date(event.end);
|
|
const hours = (end.getTime() - start.getTime()) / (1000 * 60 * 60);
|
|
return total + (isNaN(hours) ? 0 : hours);
|
|
}, 0);
|
|
|
|
return (
|
|
<main className="w-full bg-white overflow-hidden" style={{ height: 'calc(100vh - 4rem)' }}>
|
|
<div className="w-full h-full px-4 pt-12 pb-4 flex overflow-hidden">
|
|
<CalendarClient
|
|
initialCalendars={calendars}
|
|
userId={session.user.id}
|
|
userProfile={{
|
|
name: session.user.name || '',
|
|
email: session.user.email || '',
|
|
avatar: session.user.image || undefined
|
|
}}
|
|
/>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|