Files
mrp-qrcode/app/api/v1/timelogs/[id]/route.ts
T
jason 04ae88ca0d
Build and Push Docker Image / build (push) Successful in 45s
QoL changes and additions
2026-04-22 13:16:42 -05:00

94 lines
3.3 KiB
TypeScript

import { type NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
import { ok, errorResponse, requireRole, parseJson, ApiError } from "@/lib/api";
import { UpdateTimeLogSchema } from "@/lib/schemas";
import { audit } from "@/lib/audit";
import { clientIp } from "@/lib/request";
/**
* Admin-only correction of a TimeLog row. Intended for "operator forgot to
* pause overnight" cleanup — plan-vs-actual hours reports are only as good
* as the data on the floor, and a 16-hour phantom entry is worse than a
* deleted one. We audit the before/after so the raw operator entry is still
* traceable.
*
* Does NOT mutate Operation.status or claims. If the op itself is stuck
* in_progress, the admin should use the existing release/close routes.
*/
export async function PATCH(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
try {
const actor = await requireRole("admin");
const { id } = await ctx.params;
const body = await parseJson(req, UpdateTimeLogSchema);
const before = await prisma.timeLog.findUnique({ where: { id } });
if (!before) throw new ApiError(404, "not_found", "Time log not found");
// Resolve the effective startedAt / endedAt we'd persist and reject obvious
// nonsense (endedAt before startedAt). Null endedAt is still allowed —
// reopening a log for the operator to close themselves is a legitimate
// undo of a premature admin-close.
const startedAt = body.startedAt ?? before.startedAt;
const endedAt =
body.endedAt !== undefined ? body.endedAt : before.endedAt;
if (endedAt !== null && endedAt < startedAt) {
throw new ApiError(
400,
"invalid_range",
"endedAt must be on or after startedAt",
);
}
const updated = await prisma.timeLog.update({
where: { id },
data: {
...(body.startedAt !== undefined ? { startedAt: body.startedAt } : {}),
...(body.endedAt !== undefined ? { endedAt: body.endedAt } : {}),
...(body.unitsProcessed !== undefined ? { unitsProcessed: body.unitsProcessed } : {}),
...(body.note !== undefined ? { note: body.note } : {}),
},
});
await audit({
actorId: actor.id,
action: "correct_timelog",
entity: "TimeLog",
entityId: id,
before,
after: updated,
ipAddress: clientIp(req),
});
return ok({ timeLog: updated });
} catch (err) {
return errorResponse(err);
}
}
/**
* Admin-only. Deletes a TimeLog row outright — reserve for obviously-bogus
* entries (duplicate scans, test pings). Note this does NOT walk back the
* operation's `unitsCompleted` counter; if a real unit count was logged
* you almost always want to PATCH to zero it out instead.
*/
export async function DELETE(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
try {
const actor = await requireRole("admin");
const { id } = await ctx.params;
const before = await prisma.timeLog.findUnique({ where: { id } });
if (!before) throw new ApiError(404, "not_found", "Time log not found");
await prisma.timeLog.delete({ where: { id } });
await audit({
actorId: actor.id,
action: "delete_timelog",
entity: "TimeLog",
entityId: id,
before,
ipAddress: clientIp(req),
});
return ok({ ok: true });
} catch (err) {
return errorResponse(err);
}
}