All checks were successful
Deploy / deploy (push) Successful in 2m27s
- Add GET /api/period-history route with pagination, cycle length calculation, and prediction accuracy tracking - Add PATCH/DELETE /api/period-logs/[id] routes for editing and deleting period entries with ownership validation - Add /period-history page with table view, edit/delete modals, and pagination controls - Include 61 new tests covering all functionality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
150 lines
4.2 KiB
TypeScript
150 lines
4.2 KiB
TypeScript
// 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<PeriodLog>(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 });
|
|
});
|