import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/options"; import { prisma } from "@/lib/prisma"; import { getCachedCalendarData, cacheCalendarData } from "@/lib/redis"; import { logger } from "@/lib/logger"; /** * Handles the GET request to retrieve calendars for the authenticated user. * * @param {NextRequest} req - The incoming request object. * @returns {Promise} - A promise that resolves to a JSON response containing the calendars or an error message. * * The function performs the following steps: * 1. Retrieves the server session using `getServerSession`. * 2. Checks if the user is authenticated by verifying the presence of `session.user.id`. * - If not authenticated, returns a 401 response with an error message. * 3. Attempts to fetch the calendars associated with the authenticated user from the database. * - If successful, returns the calendars in a JSON response. * - If an error occurs during the database query, logs the error and returns a 500 response with an error message. */ export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); if (!session?.user?.id) { return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); } try { // Check for force refresh parameter const url = new URL(req.url); const forceRefresh = url.searchParams.get('refresh') === 'true'; // Try to get data from cache if not forcing refresh if (!forceRefresh) { const cachedData = await getCachedCalendarData(session.user.id); if (cachedData) { logger.debug('[CALENDAR] Using cached calendar data', { userId: session.user.id, calendarCount: cachedData.length, }); return NextResponse.json(cachedData); } } // If no cache or forcing refresh, fetch from database logger.debug('[CALENDAR] Fetching calendar data from database', { userId: session.user.id, }); // Ensure user has a default private calendar (created automatically if missing) const defaultPrivateCalendarName = "Mon Calendrier"; const existingDefaultCalendar = await prisma.calendar.findFirst({ where: { userId: session.user.id, name: defaultPrivateCalendarName, missionId: null, syncConfig: null, } }); if (!existingDefaultCalendar) { await prisma.calendar.create({ data: { name: defaultPrivateCalendarName, color: "#4f46e5", description: "Votre calendrier personnel", userId: session.user.id, missionId: null, } }); logger.debug('[CALENDAR] Created default private calendar', { userId: session.user.id, }); } // Get user's personal calendars // Include syncConfig to filter "Privée"/"Default" calendars that don't have active sync const personalCalendars = await prisma.calendar.findMany({ where: { userId: session.user.id, }, include: { events: { orderBy: { start: 'asc' } }, mission: { include: { missionUsers: true } }, syncConfig: { include: { mailCredential: { select: { id: true, email: true, display_name: true, } } } } }, orderBy: { createdAt: "desc", }, }); // Filter out "Privée"/"Default" calendars that don't have active sync // This matches the logic in app/agenda/page.tsx const filteredPersonalCalendars = personalCalendars.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) { logger.debug('[CALENDAR] Filtering out calendar without active sync', { calendarId: cal.id, calendarName: cal.name, syncEnabled: cal.syncConfig?.syncEnabled, hasMailCredential: !!cal.syncConfig?.mailCredential, }); return false; } return 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: { include: { events: { orderBy: { start: 'asc' } }, mission: { include: { missionUsers: true } } } } } } } }); // Extract mission calendars (excluding those already in personalCalendars) // Use a Set to avoid duplicate calendars by ID const personalCalendarIds = new Set(filteredPersonalCalendars.map(cal => cal.id)); const missionCalendars = missionUserRelations .flatMap(mu => mu.mission.calendars) .filter(cal => !personalCalendarIds.has(cal.id)); // Exclude calendars already in personalCalendars // Combine personal and mission calendars const calendars = [...filteredPersonalCalendars, ...missionCalendars]; // Remove duplicate calendars by ID (in case same calendar appears multiple times) const uniqueCalendars = Array.from( new Map(calendars.map(cal => [cal.id, cal])).values() ); // Sort calendars: "Mon Calendrier" first, then synced, then groups, then missions const sortedCalendars = uniqueCalendars.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 || ''); }); logger.debug('[CALENDAR] Fetched calendars with events', { userId: session.user.id, personalCount: filteredPersonalCalendars.length, missionCount: missionCalendars.length, totalCount: sortedCalendars.length, filteredOut: personalCalendars.length - filteredPersonalCalendars.length, }); // Cache the results await cacheCalendarData(session.user.id, sortedCalendars); return NextResponse.json(sortedCalendars); } catch (error) { logger.error('[CALENDAR] Erreur lors de la récupération des calendriers', { error: error instanceof Error ? error.message : String(error), }); return NextResponse.json({ error: "Erreur serveur" }, { status: 500 }); } } /** * Handles the POST request to create a new calendar. * * @param {NextRequest} req - The incoming request object. * @returns {Promise} The response object containing the created calendar or an error message. * * @throws {Error} If there is an issue with the request or server. * * The function performs the following steps: * 1. Retrieves the server session using `getServerSession`. * 2. Checks if the user is authenticated by verifying the presence of `session.user.id`. * 3. Parses the request body to extract `name`, `color`, and `description`. * 4. Validates that the `name` field is provided. * 5. Creates a new calendar entry in the database using Prisma. * 6. Returns the created calendar with a 201 status code. * 7. Catches and logs any errors, returning a 500 status code with an error message. */ export async function POST(req: NextRequest) { const session = await getServerSession(authOptions); if (!session?.user?.id) { return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); } try { const { name, color, description } = await req.json(); // Validation if (!name) { return NextResponse.json( { error: "Le nom du calendrier est requis" }, { status: 400 } ); } const calendar = await prisma.calendar.create({ data: { name, color: color || "#0082c9", description, userId: session.user.id, }, }); return NextResponse.json(calendar, { status: 201 }); } catch (error) { console.error("Erreur lors de la création du calendrier:", error); return NextResponse.json({ error: "Erreur serveur" }, { status: 500 }); } }