NeahStable/app/agenda/page.tsx
2026-01-14 21:26:37 +01:00

817 lines
29 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: "Enkun - 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, enkun",
};
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 || '';
// Get all calendars for the user with mission relation and sync configuration
// Exclude "Privée" and "Default" calendars that are not synced (they should only exist if synced from courrier)
let 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 active sync config
{
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,
}
}
}
}
}
});
// Auto-setup sync for email accounts from courrier (Infomaniak and Microsoft)
// Get all Infomaniak email accounts
const infomaniakAccounts = await prisma.mailCredentials.findMany({
where: {
userId: session?.user?.id || '',
host: {
contains: 'infomaniak'
},
password: {
not: null
}
},
select: {
id: true,
email: true,
display_name: true,
password: true
}
});
// 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([
...infomaniakAccounts.map(acc => acc.id),
...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 Infomaniak account, ensure there's a synced calendar
// Skip if no Infomaniak accounts exist (user may only have Microsoft accounts)
if (infomaniakAccounts.length > 0) {
for (const account of infomaniakAccounts) {
// Check if a calendar sync already exists for this account (enabled or disabled)
// This prevents creating duplicate calendars for the same account
let existingSync = await prisma.calendarSync.findFirst({
where: {
mailCredentialId: account.id
},
include: {
calendar: true
}
});
// If no sync found by mailCredentialId, check if there's an orphaned sync with same email
// This handles the case where user deleted and re-added the account (new mailCredentialId, same email)
if (!existingSync) {
const orphanedSyncByEmail = await prisma.calendarSync.findFirst({
where: {
provider: 'infomaniak',
calendar: {
userId: session?.user?.id || '',
name: 'Privée'
},
mailCredential: {
email: account.email
}
},
include: {
calendar: true,
mailCredential: true
}
});
if (orphanedSyncByEmail && orphanedSyncByEmail.mailCredential?.email === account.email) {
console.log(`[AGENDA] Found orphaned Infomaniak sync for email ${account.email}, reassigning to new mailCredentialId ${account.id}`);
// Reassign the sync to the new mailCredentialId
await prisma.calendarSync.update({
where: { id: orphanedSyncByEmail.id },
data: {
mailCredentialId: account.id,
syncEnabled: true
}
});
// Reload the sync
existingSync = await prisma.calendarSync.findFirst({
where: {
mailCredentialId: account.id
},
include: {
calendar: true
}
});
}
}
// If sync exists but is disabled, check if it's due to invalid credentials
// Don't re-enable if the last error was 401 (invalid credentials)
if (existingSync) {
console.log(`[AGENDA] Found existing sync for Infomaniak account ${account.email}: syncId=${existingSync.id}, calendarId=${existingSync.calendarId}, syncEnabled=${existingSync.syncEnabled}, hasCalendar=${!!existingSync.calendar}`);
// Check if calendar still exists
if (!existingSync.calendar) {
console.log(`[AGENDA] Calendar for sync ${existingSync.id} does not exist, creating new calendar`);
// Calendar was deleted, create a new one
const calendar = await prisma.calendar.create({
data: {
name: "Privée",
color: "#4F46E5",
description: `Calendrier synchronisé avec ${account.display_name || account.email}`,
userId: session?.user?.id || '',
}
});
// Update sync to point to new calendar
await prisma.calendarSync.update({
where: { id: existingSync.id },
data: {
calendarId: calendar.id,
syncEnabled: true
}
});
continue;
}
if (!existingSync.syncEnabled) {
const isAuthError = existingSync.lastSyncError?.includes('401') ||
existingSync.lastSyncError?.includes('Unauthorized') ||
existingSync.lastSyncError?.includes('invalid');
if (!isAuthError) {
// Only re-enable if it's not an authentication error
console.log(`[AGENDA] Re-enabling sync ${existingSync.id} for Infomaniak account ${account.email}`);
await prisma.calendarSync.update({
where: { id: existingSync.id },
data: { syncEnabled: true }
});
} else {
// Try to discover calendars to verify if credentials are now valid
// But if discovery fails and we have an existing URL, re-enable sync anyway
// The existing URL might still work even if discovery fails
try {
const { discoverInfomaniakCalendars } = await import('@/lib/services/caldav-sync');
const externalCalendars = await discoverInfomaniakCalendars(
account.email,
account.password!
);
if (externalCalendars.length > 0) {
// Credentials are now valid, re-enable sync with discovered calendar
await prisma.calendarSync.update({
where: { id: existingSync.id },
data: {
syncEnabled: true,
lastSyncError: null,
externalCalendarId: externalCalendars[0].id,
externalCalendarUrl: externalCalendars[0].url
}
});
} else if (existingSync.externalCalendarUrl) {
// Discovery succeeded but no calendars found, but we have an existing URL
// Re-enable sync with existing URL - it might still work
await prisma.calendarSync.update({
where: { id: existingSync.id },
data: {
syncEnabled: true,
lastSyncError: 'Aucun calendrier trouvé lors de la découverte, utilisation de l\'URL existante'
}
});
}
} catch (error) {
// Discovery failed, but if we have an existing URL, re-enable sync anyway
// The existing URL might still work even if discovery fails (e.g., due to network issues)
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
if (existingSync.externalCalendarUrl) {
// We have an existing URL, re-enable sync - it worked before, might still work
await prisma.calendarSync.update({
where: { id: existingSync.id },
data: {
syncEnabled: true,
lastSyncError: `Découverte CalDAV échouée (${errorMessage}), mais réactivation du sync avec l'URL existante`
}
});
} else {
// No existing URL, keep sync disabled
await prisma.calendarSync.update({
where: { id: existingSync.id },
data: {
lastSyncError: `Identifiants invalides ou expirés (${errorMessage}). Veuillez vérifier vos identifiants Infomaniak dans la page courrier.`
}
});
}
}
}
}
continue; // Skip to next account
}
if (!existingSync) {
// No sync exists for this account - try to discover and create calendar
// Only create calendar if discovery succeeds
try {
const { discoverInfomaniakCalendars } = await import('@/lib/services/caldav-sync');
const externalCalendars = await discoverInfomaniakCalendars(
account.email,
account.password!
);
if (externalCalendars.length > 0) {
// Use the first calendar (usually the main calendar)
const mainCalendar = externalCalendars[0];
console.log(`[AGENDA] Creating Infomaniak calendar for ${account.email} with URL: ${mainCalendar.url}`);
// Create a private calendar for this account
const calendar = await prisma.calendar.create({
data: {
name: "Privée",
color: "#4F46E5",
description: `Calendrier synchronisé avec ${account.display_name || account.email}`,
userId: session?.user?.id || '',
}
});
// Create sync configuration
const syncConfig = await prisma.calendarSync.create({
data: {
calendarId: calendar.id,
mailCredentialId: account.id,
provider: 'infomaniak',
externalCalendarId: mainCalendar.id,
externalCalendarUrl: mainCalendar.url,
syncEnabled: true,
syncFrequency: 15
}
});
console.log(`[AGENDA] Created Infomaniak calendar sync: ${syncConfig.id} for calendar: ${calendar.id}`);
// Trigger initial sync
try {
const { syncInfomaniakCalendar } = await import('@/lib/services/caldav-sync');
await syncInfomaniakCalendar(syncConfig.id, true);
console.log(`[AGENDA] Initial sync completed for Infomaniak calendar: ${calendar.id}`);
} catch (syncError) {
const syncErrorMessage = syncError instanceof Error ? syncError.message : 'Unknown error';
console.log(`[AGENDA] Initial sync failed for Infomaniak calendar: ${calendar.id} - ${syncErrorMessage}`);
await prisma.calendarSync.update({
where: { id: syncConfig.id },
data: {
lastSyncError: `Erreur de synchronisation: ${syncErrorMessage}`
}
});
}
}
} catch (error) {
// Discovery failed - don't create calendar
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.log(`[AGENDA] Infomaniak calendar discovery failed for ${account.email} - ${errorMessage}. Calendar will not be created.`);
// Continue with other accounts even if one fails
}
}
}
}
// 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
// Keep only the most recent one with syncEnabled=true, delete others
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
for (const [key, syncs] of syncsByAccount.entries()) {
if (syncs.length > 1) {
// Sort by syncEnabled first (enabled first), then by createdAt (newest first)
syncs.sort((a, b) => {
if (a.syncEnabled !== b.syncEnabled) {
return a.syncEnabled ? -1 : 1;
}
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
});
const keepSync = syncs[0];
const duplicates = syncs.slice(1);
// Disable or delete duplicate syncs
for (const duplicate of duplicates) {
if (duplicate.syncEnabled) {
// Disable the duplicate sync
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) {
await prisma.calendar.delete({
where: { id: duplicate.calendarId }
});
}
}
}
}
// 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 (2 minutes) for better reactivity
// This allows new events to appear faster without overloading the API
const microsoftMinSyncInterval = 2; // minutes
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 2 min or configured frequency)
const needsSync = !syncConfig.lastSyncAt ||
minutesSinceLastSync >= Math.min(microsoftMinSyncInterval, syncConfig.syncFrequency);
console.log(`[AGENDA] Microsoft sync config ${syncConfig.id}: lastSyncAt=${syncConfig.lastSyncAt}, minutesSinceLastSync=${minutesSinceLastSync.toFixed(1)}, needsSync=${needsSync}, syncFrequency=${syncConfig.syncFrequency}`);
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
// Debug: Verify Infomaniak calendars exist in database
// Check both by provider and by mailCredential email
const allInfomaniakSyncs = await prisma.calendarSync.findMany({
where: {
provider: 'infomaniak',
calendar: {
userId: session?.user?.id || ''
}
},
include: {
calendar: true,
mailCredential: true
}
});
// Also check for syncs linked to Infomaniak accounts by email (in case mailCredentialId changed)
const infomaniakEmails = infomaniakAccounts.map(acc => acc.email);
const syncsByEmail = await prisma.calendarSync.findMany({
where: {
provider: 'infomaniak',
calendar: {
userId: session?.user?.id || ''
},
mailCredential: {
email: {
in: infomaniakEmails
}
}
},
include: {
calendar: true,
mailCredential: true
}
});
console.log(`[AGENDA] Found ${allInfomaniakSyncs.length} Infomaniak syncs by provider, ${syncsByEmail.length} by email`);
[...allInfomaniakSyncs, ...syncsByEmail].forEach(sync => {
console.log(`[AGENDA] Infomaniak sync: id=${sync.id}, calendarId=${sync.calendarId}, calendarName=${sync.calendar?.name}, syncEnabled=${sync.syncEnabled}, mailCredentialId=${sync.mailCredentialId}, email=${sync.mailCredential?.email || 'none'}, hasMailCredential=${!!sync.mailCredential}`);
});
// Debug: Log calendars before filtering
console.log(`[AGENDA] Calendars before filtering: ${calendars.length}`);
const infomaniakBeforeFilter = calendars.filter(cal => cal.syncConfig?.provider === 'infomaniak');
console.log(`[AGENDA] Infomaniak calendars before filtering: ${infomaniakBeforeFilter.length}`);
infomaniakBeforeFilter.forEach(cal => {
console.log(`[AGENDA] Before filter - Calendar: ${cal.name}, id: ${cal.id}, syncEnabled: ${cal.syncConfig?.syncEnabled}, hasMailCredential: ${!!cal.syncConfig?.mailCredential}`);
});
// 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
// Log for debugging if Infomaniak calendar is missing
if (isPrivateOrDefault && cal.syncConfig?.provider === 'infomaniak') {
if (!hasActiveSync) {
console.log(`[AGENDA] Infomaniak calendar found but sync is disabled: ${cal.id}, syncEnabled: ${cal.syncConfig?.syncEnabled}, hasMailCredential: ${!!cal.syncConfig?.mailCredential}, error: ${cal.syncConfig?.lastSyncError || 'none'}`);
} else {
console.log(`[AGENDA] Infomaniak calendar is active and will be displayed: ${cal.id}, email: ${cal.syncConfig?.mailCredential?.email}`);
}
}
if (isPrivateOrDefault && !hasActiveSync) {
return false;
}
return true;
});
// Log final count of Infomaniak calendars
const infomaniakCalendars = calendars.filter(cal => cal.syncConfig?.provider === 'infomaniak');
console.log(`[AGENDA] Final Infomaniak calendars count: ${infomaniakCalendars.length}`);
// 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);
});
}
});
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 h-screen bg-white">
<div className="w-full h-full px-4 pt-12 pb-4 flex">
<CalendarClient
initialCalendars={calendars}
userId={session.user.id}
userProfile={{
name: session.user.name || '',
email: session.user.email || '',
avatar: session.user.image || undefined
}}
/>
</div>
</main>
);
}