94 lines
3.3 KiB
TypeScript
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);
|
|
}
|
|
}
|