drawings: VISUAL's rev is the source of truth; attach it when M-Files holds it Effective or Retired
When M-Files' Effective rev differs from the rev VISUAL calls, the drawing job looks that rev up (MFilesKit DrawingLookup.revision). If M-Files has it as Effective or Retired, the job copies it into drawing_revisions and attaches it to the draft with no flag and no Clerical approval, so the draft can auto-release. Past draft, already attached or with a hand drawing: unchanged, no flag and no change order. Any other status, or no document at that rev, still flags rev_mismatch with a note saying why. Packet Station only ever attaches Effective or Retired drawings. rev_approve stays for that case only. This replaces the "Use rev V as VISUAL calls it" choice from #46: the use=called path in rev-approve and its button are removed. A packet that already kept the rev that way keeps it, since that rev is now used anyway. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -116,29 +116,31 @@ describe("Packet page, VISUAL box", () => {
|
||||
expect(await screen.findByText("Rev 3 approved and attached.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("offers the retired rev VISUAL calls next to the latest, and keeps it with use=called", async () => {
|
||||
const called = { rev: "2", controlled: "Retired", state: "Superseded", name: "M00543 r2.pdf", asset_id: "as-r2", pages: 1 };
|
||||
const draft = visualDraft({ visual: { ...visualDraft().visual, staged_rev: { rev: 3, name: "M00543 r3.pdf", asset_id: "as-r3" }, called } });
|
||||
const kept = visualDraft({
|
||||
version: 4,
|
||||
it("says when the rev VISUAL calls is attached although M-Files has retired it", async () => {
|
||||
const page = { n: 1, type: "drawing", asset_id: "as-r2", pn: 1, dwg: "M00543", rev: "2", void: null, url: "/_blob/as-r2" };
|
||||
const attached = visualDraft({
|
||||
flags: [],
|
||||
flag: null,
|
||||
drawing_state: "attached",
|
||||
visual: { rev_ok: { rev: "2", vis_rev: "2", use: "called", latest: "3", controlled: "Retired", reason: "Lot 1 builds to rev 2" } },
|
||||
pages: [page] as unknown as PacketRead["pages"],
|
||||
visual: {
|
||||
appr: { ok: true, rev: 3, vrev: "2", name: "M00543 r3.pdf", note: "" },
|
||||
vault: { asset_id: "as-r2", name: "M00543 r2.pdf", rev: 2, controlled: "Retired" },
|
||||
},
|
||||
});
|
||||
const fetch = mockFetch({ ...base(draft, LEO), "POST /api/packets/1/rev-approve": { body: kept } });
|
||||
mockFetch({ ...base(attached, LEO) });
|
||||
renderApp("/packets/1");
|
||||
expect(await screen.findByRole("button", { name: "Use latest rev 3" })).toBeInTheDocument();
|
||||
expect(screen.getByText(/retired \(Superseded\) in M-Files/)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Use rev 2 as VISUAL calls it" }));
|
||||
const ask = await screen.findByRole("dialog", { name: "Use rev 2 for M00543" });
|
||||
fireEvent.change(within(ask).getByLabelText("Reason"), { target: { value: "Lot 1 builds to rev 2" } });
|
||||
fireEvent.click(within(ask).getByRole("button", { name: "Use rev 2" }));
|
||||
await sign();
|
||||
expect(callsTo(fetch, "POST", "/api/packets/1/rev-approve")).toEqual([
|
||||
{ reason: "Lot 1 builds to rev 2", use: "called", sign: { factor: "password", secret: "pw-1" } },
|
||||
]);
|
||||
expect(await screen.findByText("Rev 2 attached as VISUAL calls it.")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Rev 2 attached as VISUAL calls it")).toBeInTheDocument();
|
||||
expect(screen.getByText(/retired in M-Files, which has rev 3 as Effective/)).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /Approve rev/ })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("explains why the rev VISUAL calls is not attached when its status is not Effective or Retired", async () => {
|
||||
const draft = visualDraft({ visual: { ...visualDraft().visual, called: { rev: "2", controlled: "Obsolete", refused: true } } });
|
||||
mockFetch({ ...base(draft, LEO) });
|
||||
renderApp("/packets/1");
|
||||
expect(await screen.findByText(/VISUAL calls rev 2, which is obsolete in M-Files/)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Approve rev 3 for this packet" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uploads a hand BOM as multipart with the signature fields", async () => {
|
||||
|
||||
@@ -163,7 +163,9 @@ export function VisualBox({
|
||||
const drawingFlag = flags.some((f) => (DRAWING_FLAGS as readonly string[]).includes(f));
|
||||
const appr = v.appr;
|
||||
const staged = v.staged_rev;
|
||||
const called = v.called && !v.called.missing && v.called.asset_id ? v.called : null;
|
||||
const called = v.called ?? null;
|
||||
const vault = v.vault;
|
||||
const retiredAttached = hasDwg && !drawingFlag && !!vault?.controlled && vault.controlled.toLowerCase() !== "effective";
|
||||
const mismatch = flags.includes("rev_mismatch");
|
||||
const md = v.manual_dwg;
|
||||
const mb = v.manual_bom;
|
||||
@@ -188,25 +190,6 @@ export function VisualBox({
|
||||
await mutate((g) => packetsApi.revApprove(p.id, { reason, ...g }), { sign: SIGN_REV_APPROVE, ok: `Rev ${staged.rev} approved and attached.` });
|
||||
};
|
||||
|
||||
const keepCalled = async () => {
|
||||
if (!called) return;
|
||||
const status = (called.controlled || "not Effective").toLowerCase();
|
||||
const reason = await askReason({
|
||||
title: `Use rev ${called.rev} for ${p.drawing ?? "this drawing"}`,
|
||||
sub,
|
||||
note: `VISUAL calls rev ${called.rev}, which is ${status} in M-Files${staged ? `. The Effective rev is ${staged.rev}` : ""}. Rev ${called.rev}${called.name ? ` (${called.name})` : ""} is attached to this packet and the shop builds to it. It is recorded with your name, signature and the reason. If M-Files releases a newer rev or VISUAL's rev changes, the packet is flagged again.`,
|
||||
placeholder: `e.g. Engineering says build this lot to rev ${called.rev}`,
|
||||
min: 3,
|
||||
max: 400,
|
||||
submitLabel: `Use rev ${called.rev}`,
|
||||
});
|
||||
if (reason === null) return;
|
||||
await mutate((g) => packetsApi.revApprove(p.id, { reason, use: "called", ...g }), {
|
||||
sign: SIGN_REV_APPROVE,
|
||||
ok: `Rev ${called.rev} attached as VISUAL calls it.`,
|
||||
});
|
||||
};
|
||||
|
||||
const upload = async (kind: "drawing" | "bom", file: File, reason: string) => {
|
||||
setHand(null);
|
||||
if (kind === "bom") {
|
||||
@@ -259,6 +242,15 @@ export function VisualBox({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{retiredAttached && vault ? (
|
||||
<div className="callout flush">
|
||||
<b>
|
||||
Rev {String(vault.rev)} attached as VISUAL calls it
|
||||
</b>{" "}
|
||||
- {(vault.controlled || "").toLowerCase()} in M-Files{appr?.rev !== undefined && appr.rev !== null ? `, which has rev ${appr.rev} as Effective` : ""}.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!hasDwg || drawingFlag ? (
|
||||
<div className="callout warn flush">
|
||||
<b>{hasDwg ? "Made from VISUAL - drawing needs a look." : "Made from VISUAL - drawing not attached yet."}</b> {p.drawing || "No drawing number"} rev {p.rev || "-"}. The drawing
|
||||
@@ -280,38 +272,23 @@ export function VisualBox({
|
||||
.
|
||||
</div>
|
||||
) : null}
|
||||
{called ? (
|
||||
<div>
|
||||
VISUAL calls rev {called.rev}: <span className="mono">{called.name ?? `rev ${called.rev}`}</span>, {(called.controlled || "not Effective").toLowerCase()}
|
||||
{called.state ? ` (${called.state})` : ""} in M-Files{" "}
|
||||
<a className="lnk" href={blobUrl(called.asset_id as string)} target="_blank" rel="noopener noreferrer">
|
||||
View
|
||||
</a>
|
||||
.
|
||||
{called?.missing ? (
|
||||
<div className="muted">VISUAL calls rev {called.rev}, which is not in M-Files.</div>
|
||||
) : called?.refused ? (
|
||||
<div className="muted">
|
||||
VISUAL calls rev {called.rev}, which is {(called.controlled || "").toLowerCase()} in M-Files. Only an Effective or Retired drawing is
|
||||
attached.
|
||||
</div>
|
||||
) : v.called?.missing ? (
|
||||
<div className="muted">VISUAL calls rev {v.called.rev}, which is not in M-Files.</div>
|
||||
) : null}
|
||||
{staged || called ? (
|
||||
{staged ? (
|
||||
canApproveRev ? (
|
||||
<div className="acts">
|
||||
{staged ? (
|
||||
<button className="btn sm pri" type="button" disabled={working} onClick={() => void approveRev()}>
|
||||
{called ? `Use latest rev ${String(staged.rev)}` : `Approve rev ${String(staged.rev)} for this packet`}
|
||||
</button>
|
||||
) : null}
|
||||
{called ? (
|
||||
<button className="btn sm" type="button" disabled={working} onClick={() => void keepCalled()}>
|
||||
Use rev {called.rev} as VISUAL calls it
|
||||
</button>
|
||||
) : null}
|
||||
<button className="btn sm pri" type="button" disabled={working} onClick={() => void approveRev()}>
|
||||
Approve rev {String(staged.rev)} for this packet
|
||||
</button>
|
||||
</div>
|
||||
) : draft ? (
|
||||
<span className="muted">
|
||||
{called && staged
|
||||
? `A Clerical supervisor can choose rev ${staged.rev} or rev ${called.rev} for this packet.`
|
||||
: `A Clerical supervisor can approve rev ${String((staged ?? called)?.rev)} for this packet.`}
|
||||
</span>
|
||||
<span className="muted">A Clerical supervisor can approve rev {String(staged.rev)} for this packet.</span>
|
||||
) : null
|
||||
) : (
|
||||
<span className="muted">The M-Files drawing is being copied. Check back after the next drawing run.</span>
|
||||
|
||||
Reference in New Issue
Block a user