CRITICAL BUG FIX: - Phase boundaries were hardcoded for 31-day cycle, breaking correct phase calculations for users with different cycle lengths (28, 35, etc.) - Added getPhaseBoundaries(cycleLength) function in cycle.ts - Updated getPhase() to accept cycleLength parameter (default 31) - Updated all callers (API routes, components) to pass cycleLength - Added 13 new tests for phase boundaries with 28, 31, and 35-day cycles ICS IMPROVEMENTS: - Fixed emojis to match calendar.md spec: 🩸🌱🌸🌙🌑 - Added CATEGORIES field for calendar app colors per spec: MENSTRUAL=Red, FOLLICULAR=Green, OVULATION=Pink, EARLY_LUTEAL=Yellow, LATE_LUTEAL=Orange - Added 5 new tests for CATEGORIES Updated IMPLEMENTATION_PLAN.md with discovered issues and test counts. 825 tests passing (up from 807) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
130 lines
3.9 KiB
TypeScript
130 lines
3.9 KiB
TypeScript
// ABOUTME: API route for logging period start dates.
|
|
// ABOUTME: Recalculates all phase dates when period is logged.
|
|
import type { NextRequest } from "next/server";
|
|
import { NextResponse } from "next/server";
|
|
|
|
import { withAuth } from "@/lib/auth-middleware";
|
|
import { getCycleDay, getPhase } from "@/lib/cycle";
|
|
import { logger } from "@/lib/logger";
|
|
import { createPocketBaseClient } from "@/lib/pocketbase";
|
|
|
|
interface PeriodLogRequest {
|
|
startDate?: string;
|
|
}
|
|
|
|
/**
|
|
* Validates a date string is in YYYY-MM-DD format and represents a valid date.
|
|
*/
|
|
function isValidDateFormat(dateStr: string): boolean {
|
|
const regex = /^\d{4}-\d{2}-\d{2}$/;
|
|
if (!regex.test(dateStr)) {
|
|
return false;
|
|
}
|
|
const date = new Date(dateStr);
|
|
return !Number.isNaN(date.getTime());
|
|
}
|
|
|
|
/**
|
|
* Checks if a date is in the future (after today).
|
|
*/
|
|
function isFutureDate(dateStr: string): boolean {
|
|
const inputDate = new Date(dateStr);
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
inputDate.setHours(0, 0, 0, 0);
|
|
return inputDate > today;
|
|
}
|
|
|
|
export const POST = withAuth(async (request: NextRequest, user) => {
|
|
try {
|
|
const body = (await request.json()) as PeriodLogRequest;
|
|
|
|
// Validate startDate is present
|
|
if (!body.startDate) {
|
|
return NextResponse.json(
|
|
{ error: "startDate is required" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Validate date format
|
|
if (!isValidDateFormat(body.startDate)) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid date format. Use YYYY-MM-DD" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Validate date is not in the future
|
|
if (isFutureDate(body.startDate)) {
|
|
return NextResponse.json(
|
|
{ error: "startDate cannot be in the future" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const pb = createPocketBaseClient();
|
|
|
|
// Calculate predicted date based on previous cycle (if exists)
|
|
let predictedDateStr: string | null = null;
|
|
if (user.lastPeriodDate) {
|
|
const previousPeriod = new Date(user.lastPeriodDate);
|
|
const predictedDate = new Date(previousPeriod);
|
|
predictedDate.setDate(previousPeriod.getDate() + user.cycleLength);
|
|
predictedDateStr = predictedDate.toISOString().split("T")[0];
|
|
}
|
|
|
|
// Update user's lastPeriodDate
|
|
await pb.collection("users").update(user.id, {
|
|
lastPeriodDate: body.startDate,
|
|
});
|
|
|
|
// Create PeriodLog record with prediction data
|
|
await pb.collection("period_logs").create({
|
|
user: user.id,
|
|
startDate: body.startDate,
|
|
predictedDate: predictedDateStr,
|
|
});
|
|
|
|
// Calculate updated cycle information
|
|
const lastPeriodDate = new Date(body.startDate);
|
|
const cycleDay = getCycleDay(lastPeriodDate, user.cycleLength, new Date());
|
|
const phase = getPhase(cycleDay, user.cycleLength);
|
|
|
|
// Calculate prediction accuracy
|
|
let daysEarly: number | undefined;
|
|
let daysLate: number | undefined;
|
|
if (predictedDateStr) {
|
|
const actual = new Date(body.startDate);
|
|
const predicted = new Date(predictedDateStr);
|
|
const diffDays = Math.floor(
|
|
(predicted.getTime() - actual.getTime()) / (1000 * 60 * 60 * 24),
|
|
);
|
|
if (diffDays > 0) {
|
|
daysEarly = diffDays;
|
|
} else if (diffDays < 0) {
|
|
daysLate = Math.abs(diffDays);
|
|
}
|
|
}
|
|
|
|
// Log successful period logging per observability spec
|
|
logger.info({ userId: user.id, date: body.startDate }, "Period logged");
|
|
|
|
return NextResponse.json({
|
|
message: "Period start date logged successfully",
|
|
lastPeriodDate: body.startDate,
|
|
cycleDay,
|
|
phase,
|
|
...(predictedDateStr && { predictedDate: predictedDateStr }),
|
|
...(daysEarly !== undefined && { daysEarly }),
|
|
...(daysLate !== undefined && { daysLate }),
|
|
});
|
|
} catch (error) {
|
|
logger.error({ err: error, userId: user.id }, "Period logging error");
|
|
return NextResponse.json(
|
|
{ error: "Failed to update period date" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
});
|