diff --git a/src/app/api/employees/[id]/employer/route.test.ts b/src/app/api/employees/[id]/employer/route.test.ts index d55e601..6740f94 100644 --- a/src/app/api/employees/[id]/employer/route.test.ts +++ b/src/app/api/employees/[id]/employer/route.test.ts @@ -33,9 +33,15 @@ it("refuses a worker outside the user's sites", async () => { m.user.mockResolvedValue({ role: "site_user", siteIds: [] }); expect((await POST(request(valid), context)).status).toBe(404); expect(m.create).not.toHaveBeenCalled(); }); -it.each(["2026-02-30", "2099-01-01", "bad"])("rejects invalid or future date %s", async effectiveFrom => { +it.each(["2026-02-30", "bad"])("rejects invalid date %s", async effectiveFrom => { expect((await POST(request({ ...valid, effectiveFrom }), context)).status).toBe(400); }); +it("allows backdating before the most recent assignment", async () => { + m.list.mockResolvedValue([{ id: "today", effective_from: "2026-10-29 00:00:00.000Z", employer: broker, revision: 1 }]); + const response = await POST(request({ ...valid, effectiveFrom: "2026-07-01", expectedAssignmentId: "today" }), context); + expect(response.status).toBe(200); + expect((await response.json()).assignment).toMatchObject({ effective_from: "2026-07-01 00:00:00.000Z", revision: 2 }); +}); it("refuses stale edits instead of silently replacing a newer assignment", async () => { m.list.mockResolvedValue([{ id: "newer", effective_from: "2026-10-02", employer: broker }]); expect((await POST(request(valid), context)).status).toBe(409); expect(m.create).not.toHaveBeenCalled(); diff --git a/src/app/api/employees/[id]/employer/route.ts b/src/app/api/employees/[id]/employer/route.ts index 2b127cb..f29ae6c 100644 --- a/src/app/api/employees/[id]/employer/route.ts +++ b/src/app/api/employees/[id]/employer/route.ts @@ -3,7 +3,6 @@ import { pbAdmin } from "@/lib/pb"; import { requireApiUser } from "@/lib/auth/guard"; import { canAccessSite } from "@/lib/auth/access"; import { isSameOrigin } from "@/lib/agent/same-origin"; -import { workDate } from "@/lib/clock-day"; import type { EmployerAssignment } from "@/lib/employer-assignments"; export const runtime = "nodejs"; @@ -23,7 +22,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: if (!/^[a-z0-9]{15}$/.test(id) || !parsed.success) return fail("Choose an employer/broker and an effective date.", 400); const { employer, effectiveFrom, expectedAssignmentId } = parsed.data; const date = new Date(`${effectiveFrom}T00:00:00.000Z`); - if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== effectiveFrom || effectiveFrom > workDate(new Date().toISOString())) return fail("Choose a valid date no later than today.", 400); + if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== effectiveFrom) return fail("Choose a valid date.", 400); try { const pb = await pbAdmin(); const employee = await pb.collection("employees").getOne(id); @@ -37,10 +36,10 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: }); const latest = history[0]; if ((latest?.id ?? "") !== expectedAssignmentId) return fail("This worker's assignment changed. Refresh the page and reopen Edit.", 409); - if (latest && effectiveFrom < latest.effective_from.slice(0, 10)) return fail("Choose a date on or after the most recent assignment to preserve its history.", 409); const payload = { employee: id, site: employee.site, employer, effective_from: `${effectiveFrom} 00:00:00.000Z`, revision: (latest?.revision ?? 0) + 1 }; // The unique (employee, site, revision) index makes concurrent appends atomic: - // only one editor can claim the next revision. Same-day corrections never overwrite. + // only one editor can claim the next revision. Corrections never overwrite: a + // backdated row covers the earlier days, later-dated rows still win from their date. const assignment = await pb.collection("employee_employer_assignments").create(payload); return Response.json({ assignment }); } catch (cause) { diff --git a/src/components/worker-employer-edit.tsx b/src/components/worker-employer-edit.tsx index cd1cc47..ef8e246 100644 --- a/src/components/worker-employer-edit.tsx +++ b/src/components/worker-employer-edit.tsx @@ -55,9 +55,9 @@ export function WorkerEmployerEdit({ worker, employers, history = [], onSaved }: {!siteEmployers.length &&
No employers/brokers on this site yet. Use Add Employer/Broker on the Workers page to create one.
} -Earlier dates keep their previous employer/broker. For a first assignment, choose the date this worker started with the employer. Saving again on the same date corrects that day’s assignment.
+Any date can be chosen. Days before it keep their previous employer/broker; a later dated change still applies from its own date. To fix a wrong start date, pick the correct one and save.
{error &&{error}
}