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>
186 lines
5.0 KiB
TypeScript
186 lines
5.0 KiB
TypeScript
// ABOUTME: API route for editing and deleting individual period logs.
|
|
// ABOUTME: PATCH updates startDate, DELETE removes period entry.
|
|
import type { NextRequest } from "next/server";
|
|
import { NextResponse } from "next/server";
|
|
import type PocketBase from "pocketbase";
|
|
|
|
import { withAuth } from "@/lib/auth-middleware";
|
|
import type { PeriodLog, User } from "@/types";
|
|
|
|
// Date format regex: YYYY-MM-DD
|
|
const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
|
interface RouteContext {
|
|
params?: Promise<{ id: string }>;
|
|
}
|
|
|
|
// Helper to format date as YYYY-MM-DD
|
|
function formatDateStr(date: Date): string {
|
|
return date.toISOString().split("T")[0];
|
|
}
|
|
|
|
// Helper to check if a period log is the most recent for a user
|
|
async function isMostRecentPeriod(
|
|
pb: PocketBase,
|
|
userId: string,
|
|
periodLogId: string,
|
|
): Promise<boolean> {
|
|
const result = await pb.collection("period_logs").getList<PeriodLog>(1, 1, {
|
|
filter: `user="${userId}"`,
|
|
sort: "-startDate",
|
|
});
|
|
|
|
if (result.items.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return result.items[0].id === periodLogId;
|
|
}
|
|
|
|
// Helper to get the most recent period after deletion
|
|
async function getMostRecentPeriodAfterDeletion(
|
|
pb: PocketBase,
|
|
userId: string,
|
|
): Promise<PeriodLog | null> {
|
|
const result = await pb.collection("period_logs").getList<PeriodLog>(1, 1, {
|
|
filter: `user="${userId}"`,
|
|
sort: "-startDate",
|
|
});
|
|
|
|
return result.items[0] || null;
|
|
}
|
|
|
|
export const PATCH = withAuth(
|
|
async (
|
|
request: NextRequest,
|
|
user: User,
|
|
pb: PocketBase,
|
|
context?: RouteContext,
|
|
) => {
|
|
// Get ID from route params
|
|
const { id } = await (context?.params ?? Promise.resolve({ id: "" }));
|
|
|
|
// Fetch the period log
|
|
let periodLog: PeriodLog;
|
|
try {
|
|
periodLog = await pb.collection("period_logs").getOne<PeriodLog>(id);
|
|
} catch (error) {
|
|
// Handle PocketBase 404 errors (can be Error or plain object)
|
|
const err = error as { status?: number };
|
|
if (err.status === 404) {
|
|
return NextResponse.json(
|
|
{ error: "Period log not found" },
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
// Check ownership
|
|
if (periodLog.user !== user.id) {
|
|
return NextResponse.json({ error: "Access denied" }, { status: 403 });
|
|
}
|
|
|
|
// Parse request body
|
|
const body = await request.json();
|
|
|
|
// Validate startDate is present
|
|
if (!body.startDate) {
|
|
return NextResponse.json(
|
|
{ error: "startDate is required" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Validate startDate format
|
|
if (!DATE_REGEX.test(body.startDate)) {
|
|
return NextResponse.json(
|
|
{ error: "startDate must be in YYYY-MM-DD format" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Validate startDate is a valid date
|
|
const parsedDate = new Date(body.startDate);
|
|
if (Number.isNaN(parsedDate.getTime())) {
|
|
return NextResponse.json(
|
|
{ error: "startDate is not a valid date" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Validate startDate is not in the future
|
|
const today = new Date();
|
|
today.setHours(23, 59, 59, 999); // End of today
|
|
if (parsedDate > today) {
|
|
return NextResponse.json(
|
|
{ error: "startDate cannot be in the future" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Update the period log
|
|
const updatedPeriodLog = await pb
|
|
.collection("period_logs")
|
|
.update<PeriodLog>(id, {
|
|
startDate: body.startDate,
|
|
});
|
|
|
|
// If this is the most recent period, update user.lastPeriodDate
|
|
const isLatest = await isMostRecentPeriod(pb, user.id, id);
|
|
if (isLatest) {
|
|
await pb.collection("users").update(user.id, {
|
|
lastPeriodDate: body.startDate,
|
|
});
|
|
}
|
|
|
|
return NextResponse.json(updatedPeriodLog, { status: 200 });
|
|
},
|
|
);
|
|
|
|
export const DELETE = withAuth(
|
|
async (
|
|
_request: NextRequest,
|
|
user: User,
|
|
pb: PocketBase,
|
|
context?: RouteContext,
|
|
) => {
|
|
// Get ID from route params
|
|
const { id } = await (context?.params ?? Promise.resolve({ id: "" }));
|
|
|
|
// Fetch the period log
|
|
let periodLog: PeriodLog;
|
|
try {
|
|
periodLog = await pb.collection("period_logs").getOne<PeriodLog>(id);
|
|
} catch (error) {
|
|
// Handle PocketBase 404 errors (can be Error or plain object)
|
|
const err = error as { status?: number };
|
|
if (err.status === 404) {
|
|
return NextResponse.json(
|
|
{ error: "Period log not found" },
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
// Check ownership
|
|
if (periodLog.user !== user.id) {
|
|
return NextResponse.json({ error: "Access denied" }, { status: 403 });
|
|
}
|
|
|
|
// Delete the period log
|
|
await pb.collection("period_logs").delete(id);
|
|
|
|
// Update user.lastPeriodDate to the previous period (or null if no more periods)
|
|
const previousPeriod = await getMostRecentPeriodAfterDeletion(pb, user.id);
|
|
await pb.collection("users").update(user.id, {
|
|
lastPeriodDate: previousPeriod
|
|
? formatDateStr(new Date(previousPeriod.startDate))
|
|
: null,
|
|
});
|
|
|
|
return NextResponse.json({ success: true }, { status: 200 });
|
|
},
|
|
);
|