This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user