feat(workforce): allow any effective date when editing a worker's employer/broker
Backdating before the latest assignment and future dates were refused, so a wrong start date could never be corrected. Corrections still append a revision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e9edc1988e
commit
6ba4f5be6c
3 changed files with 12 additions and 7 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@ export function WorkerEmployerEdit({ worker, employers, history = [], onSaved }:
|
|||
</label>
|
||||
{!siteEmployers.length && <p className="text-sm text-muted-foreground">No employers/brokers on this site yet. Use Add Employer/Broker on the Workers page to create one.</p>}
|
||||
<label className="block text-sm font-medium">Effective from
|
||||
<input type="date" required min={latest?.effective_from.slice(0, 10)} max={today} value={date} disabled={busy} onChange={event => setDate(event.target.value)} className="mt-2 block w-full rounded-md border bg-background px-3 py-2" />
|
||||
<input type="date" required value={date} disabled={busy} onChange={event => setDate(event.target.value)} className="mt-2 block w-full rounded-md border bg-background px-3 py-2" />
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">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.</p>
|
||||
<p className="text-sm text-muted-foreground">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.</p>
|
||||
{error && <p role="alert" className="text-sm text-destructive">{error}</p>}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" disabled={busy} onClick={() => setOpen(false)}>Cancel</Button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue