// ABOUTME: API route for retrieving period history with calculated cycle lengths. // ABOUTME: GET /api/period-history returns paginated period logs with cycle statistics. import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { withAuth } from "@/lib/auth-middleware"; import type { PeriodLog } from "@/types"; // Pagination constants const MIN_PAGE = 1; const MIN_LIMIT = 1; const MAX_LIMIT = 100; const DEFAULT_LIMIT = 20; interface PeriodLogWithCycleLength extends PeriodLog { cycleLength: number | null; daysEarly: number | null; daysLate: number | null; } interface PeriodHistoryResponse { items: PeriodLogWithCycleLength[]; total: number; page: number; limit: number; totalPages: number; hasMore: boolean; averageCycleLength: number | null; } function calculateDaysBetween(date1: Date, date2: Date): number { const d1 = new Date(date1); const d2 = new Date(date2); const diffTime = Math.abs(d1.getTime() - d2.getTime()); return Math.round(diffTime / (1000 * 60 * 60 * 24)); } export const GET = withAuth(async (request: NextRequest, user, pb) => { const { searchParams } = new URL(request.url); // Parse and validate pagination parameters const pageParam = searchParams.get("page"); const limitParam = searchParams.get("limit"); let page = MIN_PAGE; let limit = DEFAULT_LIMIT; // Validate page parameter if (pageParam !== null) { const parsedPage = Number.parseInt(pageParam, 10); if (Number.isNaN(parsedPage) || parsedPage < MIN_PAGE) { return NextResponse.json( { error: "Invalid page: must be a positive integer" }, { status: 400 }, ); } page = parsedPage; } // Validate limit parameter if (limitParam !== null) { const parsedLimit = Number.parseInt(limitParam, 10); if ( Number.isNaN(parsedLimit) || parsedLimit < MIN_LIMIT || parsedLimit > MAX_LIMIT ) { return NextResponse.json( { error: `Invalid limit: must be between ${MIN_LIMIT} and ${MAX_LIMIT}`, }, { status: 400 }, ); } limit = parsedLimit; } // Query period logs for user const result = await pb .collection("period_logs") .getList(page, limit, { filter: `user="${user.id}"`, sort: "-startDate", }); // Calculate cycle lengths between consecutive periods // Periods are sorted by startDate descending (most recent first) const itemsWithCycleLength: PeriodLogWithCycleLength[] = result.items.map( (log, index) => { let cycleLength: number | null = null; // If there's a next period (earlier period), calculate cycle length const nextPeriod = result.items[index + 1]; if (nextPeriod) { cycleLength = calculateDaysBetween(log.startDate, nextPeriod.startDate); } // Calculate prediction accuracy let daysEarly: number | null = null; let daysLate: number | null = null; if (log.predictedDate) { const actualDate = new Date(log.startDate); const predictedDate = new Date(log.predictedDate); const diffDays = Math.round( (actualDate.getTime() - predictedDate.getTime()) / (1000 * 60 * 60 * 24), ); if (diffDays < 0) { daysEarly = Math.abs(diffDays); daysLate = 0; } else { daysEarly = 0; daysLate = diffDays; } } return { ...log, cycleLength, daysEarly, daysLate, }; }, ); // Calculate average cycle length (only if we have at least 2 periods) const cycleLengths = itemsWithCycleLength .map((log) => log.cycleLength) .filter((length): length is number => length !== null); const averageCycleLength = cycleLengths.length > 0 ? Math.round( cycleLengths.reduce((sum, len) => sum + len, 0) / cycleLengths.length, ) : null; const response: PeriodHistoryResponse = { items: itemsWithCycleLength, total: result.totalItems, page: result.page, limit, totalPages: result.totalPages, hasMore: result.page < result.totalPages, averageCycleLength, }; return NextResponse.json(response, { status: 200 }); });