Files
phaseflow/src/app/api/period-history/route.ts
Petru Paler 07577dbdbb
All checks were successful
Deploy / deploy (push) Successful in 2m27s
Add period history UI with CRUD operations
- 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>
2026-01-12 22:33:36 +00:00

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 });
});