Files
phaseflow/src/app/api/period-logs/[id]/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

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