Add period history UI with CRUD operations
All checks were successful
Deploy / deploy (push) Successful in 2m27s
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>
This commit is contained in:
185
src/app/api/period-logs/[id]/route.ts
Normal file
185
src/app/api/period-logs/[id]/route.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
// 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 });
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user