Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
ci / server (pull_request) Successful in 12m48s
ci / frontend (pull_request) Successful in 54s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Successful in 4m38s

- C1 (issue 1): over target follows VISUAL ticket hours, not the timer.
- C2 (issue 24): a Q.C. reject always needs a reason; accept needs an NCR number after a reject without one.
- C3 (issue 25): an incomplete unit ITP blocks filing.
- C4 (issue 27): prints open the print dialog on the page, no new tab.
- C5 (issue 31): filing is blocked while VISUAL shows a work order open; the feed sets vclosed on VISUAL packets.
- C6 (issue 8): the data book leaves out the history pages.
- C7 (issue 18): the supervisor approves each received short; accept and filing wait for it.
- C8 (issue 23): per-department checklists, edited by the department supervisor or Admin, frozen at START.
- C9 (issue 32): Q.C. and Clerical may run the stamp check.
- C10 (issue 33): the PM screen and reports are for PS-ProjectMgmt and Admin only.
- C11 (issue 36): the Stall screen lists the packets assigned to the welder, at the supervisor's stall.

Migration 0011. Docs: PLAN (also issue 9 confirmed, issue 4 asked), POLICY, SECURITY, SCHEMA, FEED, README, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-30 16:36:49 -06:00
co-authored by Claude Opus 5.5
parent 150c507a31
commit 70a97398f1
101 changed files with 3871 additions and 1098 deletions
+43 -46
View File
@@ -79,12 +79,13 @@ afterEach(() => {
});
describe("Close and file: ready to close", () => {
it("shows the state, the exports, and confirms the close", async () => {
it("a hand packet: shows the state, the exports, and Clerical confirms the close", async () => {
const open = vi.spyOn(window, "open").mockImplementation(() => null);
const fetch = mockFetch(base(toClose(), { "POST /api/packets/1/vclosed": { body: toFile({ version: 4 }) } }));
const hand = { source_kind: "hand" as const, vclose_check: null };
const fetch = mockFetch(base(toClose(hand), { "POST /api/packets/1/vclosed": { body: toFile({ version: 4, ...hand }) } }));
renderApp("/packets/1");
const box = await panel();
expect(box).toHaveTextContent("Back from the shop, ready to close.");
expect(box).toHaveTextContent("Back from the shop, ready to close. Close the job in VISUAL, then click below.");
const file = within(box).getByRole("button", { name: "File in the data book" });
expect(file).toBeDisabled();
expect(within(box).getByText("Close the job in VISUAL first.")).toHaveClass("why");
@@ -102,10 +103,30 @@ describe("Close and file: ready to close", () => {
await waitFor(() => expect(screen.getByRole("region", { name: "Close and file" })).toHaveTextContent("Ready for the data book."));
expect(callsTo(fetch, "POST", "/api/packets/1/vclosed")).toEqual([{ closed: true }]);
expect(screen.getByRole("region", { name: "Close and file" })).toHaveTextContent(/Job closed in VISUAL .* by Clara Clerk\./);
expect(screen.getByRole("button", { name: "Not closed in VISUAL, move back" })).toBeEnabled();
});
it("a VISUAL packet waits for the feed: no manual close, File says why it is blocked", async () => {
const why = "VISUAL still shows a work order open on this packet. Close it in VISUAL first.";
mockFetch(base(toClose({ blocked: { file: [why] }, vclose_check: check({ open_wos: ["L999001/001"] }) })));
renderApp("/packets/1");
const box = await panel();
expect(box).toHaveTextContent("Back from the shop, ready to close. Close the job in VISUAL. The packet moves on to the data book when the feed sees it closed.");
expect(within(box).queryByRole("button", { name: "Job closed in VISUAL" })).not.toBeInTheDocument();
expect(within(box).getByRole("button", { name: "File in the data book" })).toBeDisabled();
expect(within(box).getByText(why)).toHaveClass("why");
});
it("a VISUAL packet the feed saw closed is ready for the data book", async () => {
mockFetch(base(toClose({ can: ["file"], blocked: {}, vclose_check: check({ closed_at: "2026-10-17T06:00:00Z", agrees: false }) })));
renderApp("/packets/1");
const box = await panel();
expect(box).toHaveTextContent(/^Ready for the data book\. VISUAL shows the job closed since .+\. Check the data book, then file it\./);
expect(within(box).getByRole("button", { name: "File in the data book" })).toBeEnabled();
expect(within(box).queryByRole("button", { name: "Not closed in VISUAL, move back" })).not.toBeInTheDocument();
});
it.each([
["the feed saw the job closed", check({ closed_at: "2026-10-17T06:00:00Z", agrees: false }), /^VISUAL shows the job closed since .+\.$/],
["work orders still open", check({ open_wos: ["L999001/001", "L999001/002"] }), /^VISUAL still shows 2 work orders open: L999001\/001, L999001\/002\.$/],
["one work order open", check({ open_wos: ["L999001/001"] }), /^VISUAL still shows 1 work order open: L999001\/001\.$/],
["the feed has not looked", check({ observed_at: null }), /^The feed has not checked VISUAL yet\.$/],
@@ -145,8 +166,9 @@ describe("Close and file: ready for the data book", () => {
);
renderApp("/packets/1");
const box = await panel();
expect(box).toHaveTextContent("Ready for the data book.");
expect(within(box).getByRole("button", { name: "Not closed in VISUAL, move back" })).toBeEnabled();
expect(box).toHaveTextContent(/^Ready for the data book\. Job closed in VISUAL .+ by Clara Clerk\./);
// A VISUAL packet: the feed decides, so there is no manual move back.
expect(within(box).queryByRole("button", { name: "Not closed in VISUAL, move back" })).not.toBeInTheDocument();
fireEvent.click(within(box).getByRole("button", { name: "File in the data book" }));
expect(await within(await screen.findByRole("dialog", { name: "Sign" })).findByText("File in the data book")).toBeInTheDocument();
await sign();
@@ -157,57 +179,32 @@ describe("Close and file: ready for the data book", () => {
]);
});
it("warns in a callout when VISUAL still shows work orders open", async () => {
mockFetch(base(toFile({ vclose_check: check({ open_wos: ["L999001/001"], agrees: false }) })));
it("File is blocked while VISUAL shows work orders open, with no second confirmation", async () => {
const why = "VISUAL still shows a work order open on this packet. Close it in VISUAL first.";
const fetch = mockFetch(base(toFile({ can: [], blocked: { file: [why] }, vclose_check: check({ open_wos: ["L999001/001"], agrees: false }) })));
renderApp("/packets/1");
const box = await panel();
const line = within(box).getByTestId("visual-line");
expect(line).toHaveClass("callout", "warn");
expect(line).toHaveTextContent("VISUAL still shows 1 work order open: L999001/001.");
const file = within(box).getByRole("button", { name: "File in the data book" });
expect(file).toBeDisabled();
expect(within(box).getByText(why)).toHaveClass("why");
fireEvent.click(file);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
expect(callsTo(fetch, "POST", "/api/packets/1/transitions")).toHaveLength(0);
});
it("asks on 409 open_work_orders and sends it again with confirm_open_wos", async () => {
const msg = "VISUAL still shows 1 work order open on this packet: L999001/001. File anyway?";
it("shows the server's refusal when filing is refused after all", async () => {
const detail = "VISUAL still shows a work order open on this packet. Close it in VISUAL first.";
const fetch = mockFetch(
base(toFile({ vclose_check: check({ open_wos: ["L999001/001"], agrees: false }) }), {
"POST /api/packets/1/transitions": (req) => {
const b = req.body as { sign?: unknown; confirm_open_wos?: boolean };
if (!b.confirm_open_wos) return { status: 409, body: { code: "open_work_orders", detail: msg, work_orders: ["L999001/001"] } };
if (!b.sign) return { status: 400, body: { code: "signature_required", detail: "This step needs your signature.", meaning: "File in the data book" } };
return { body: filed({ seal: seal({ open_wos: ["L999001/001"] }) }) };
},
"/api/packets/1/seal": { body: sealView(true, seal({ open_wos: ["L999001/001"] })) },
}),
base(toFile(), { "POST /api/packets/1/transitions": { status: 409, body: { code: "precondition_failed", detail } } }),
);
renderApp("/packets/1");
fireEvent.click(within(await panel()).getByRole("button", { name: "File in the data book" }));
const sheet = await screen.findByRole("dialog", { name: "File with open work orders?" });
expect(within(sheet).getByText(msg)).toBeInTheDocument();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
fireEvent.click(within(sheet).getByRole("button", { name: "File anyway" }));
await sign();
await waitFor(() => expect(screen.getByRole("region", { name: "Close and file" })).toHaveTextContent("Filed in the data book"));
expect(callsTo(fetch, "POST", "/api/packets/1/transitions")).toEqual([
{ action: "file", expect: { status: "complete", dept: "03" } },
{ action: "file", expect: { status: "complete", dept: "03" }, confirm_open_wos: true },
{ action: "file", expect: { status: "complete", dept: "03" }, confirm_open_wos: true, sign: { factor: "password", secret: "pw-1" } },
]);
expect(screen.getByRole("region", { name: "Close and file" })).toHaveTextContent("Filed with work orders open in VISUAL: L999001/001.");
});
it("Cancel on the confirm sends nothing more", async () => {
const fetch = mockFetch(
base(toFile(), {
"POST /api/packets/1/transitions": { status: 409, body: { code: "open_work_orders", detail: "VISUAL still shows 1 work order open.", work_orders: ["L999001/001"] } },
}),
);
renderApp("/packets/1");
fireEvent.click(within(await panel()).getByRole("button", { name: "File in the data book" }));
const sheet = await screen.findByRole("dialog", { name: "File with open work orders?" });
fireEvent.click(within(sheet).getByRole("button", { name: "Cancel" }));
await waitFor(() => expect(screen.queryByRole("dialog", { name: "File with open work orders?" })).not.toBeInTheDocument());
expect(callsTo(fetch, "POST", "/api/packets/1/transitions")).toHaveLength(1);
expect(screen.getByRole("region", { name: "Close and file" })).toHaveTextContent("Ready for the data book.");
expect(await screen.findByRole("alert")).toHaveTextContent(detail);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
expect(callsTo(fetch, "POST", "/api/packets/1/transitions")).toEqual([{ action: "file", expect: { status: "complete", dept: "03" } }]);
});
});