2026-10-16 22:46:56 -06:00
|
|
|
import { useState } from "react";
|
|
|
|
|
import { ApiError } from "../api/client";
|
|
|
|
|
import { packetsApi } from "../api/packets";
|
|
|
|
|
import type { PacketRead, Weld, WeldBody } from "../api/types";
|
|
|
|
|
import type { ReasonOptions } from "../components/ReasonSheet";
|
|
|
|
|
import { errorText } from "../lib/format";
|
|
|
|
|
import { voidText } from "../lib/voidStamp";
|
|
|
|
|
import { weldId, weldSummary } from "../lib/weldDefaults";
|
|
|
|
|
import type { Write, WriteResult } from "../warehouse/PullTable";
|
|
|
|
|
import { MapViewer } from "./MapViewer";
|
2026-10-17 23:49:14 -06:00
|
|
|
import { openPrint } from "./closeUtil";
|
2026-10-16 22:46:56 -06:00
|
|
|
import { WeldMark, type MarkMove } from "./marks";
|
|
|
|
|
import { landingPage, pageLabel, usePeople, viewPages } from "./shopUtil";
|
|
|
|
|
import { useSerial } from "./useSerial";
|
|
|
|
|
import { WeldRecord } from "./WeldRecord";
|
|
|
|
|
import { WeldSheet, type NewWeldAt } from "./WeldSheet";
|
|
|
|
|
import type { Mutate } from "./tabs";
|
|
|
|
|
|
|
|
|
|
const rv = (cur: PacketRead, no: number, fallback: number) => cur.welds.find((w) => w.weld_no === no)?.row_version ?? fallback;
|
|
|
|
|
|
|
|
|
|
function WeldRow({ w, onOpen }: { w: Weld; onOpen: (w: Weld) => void }) {
|
|
|
|
|
return (
|
|
|
|
|
<button type="button" className="row rowbtn" onClick={() => onOpen(w)} aria-label={`Open weld ${weldId(w)}`}>
|
|
|
|
|
<span className={`balloon pinkb${w.carried ? " carried" : ""}`}>{w.weld_no}</span>
|
|
|
|
|
<span className="d">
|
|
|
|
|
{weldSummary(w)}
|
|
|
|
|
<div className="small muted mono">
|
|
|
|
|
{w.wo_base ?? "-"} #{w.piece ?? 1} · {w.process ?? "-"} · {w.stamp ?? ""}
|
|
|
|
|
</div>
|
|
|
|
|
</span>
|
|
|
|
|
<span />
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The weld map: the drawing with the welds of the open sheet as pink leader marks. "Mark a weld" turns on
|
|
|
|
|
* marking; a tap on the joint opens the weld sheet. Bubbles and arrows drag when the user may edit welds.
|
|
|
|
|
*/
|
|
|
|
|
export function WeldMapTab({
|
|
|
|
|
p,
|
|
|
|
|
write,
|
|
|
|
|
mutate,
|
|
|
|
|
reload,
|
|
|
|
|
askReason,
|
|
|
|
|
sub,
|
|
|
|
|
myId,
|
|
|
|
|
onError,
|
|
|
|
|
}: {
|
|
|
|
|
p: PacketRead;
|
|
|
|
|
write: Write;
|
|
|
|
|
mutate: Mutate;
|
|
|
|
|
reload: () => Promise<void>;
|
|
|
|
|
askReason: (o: ReasonOptions) => Promise<string | null>;
|
|
|
|
|
sub: string;
|
|
|
|
|
myId: string | null;
|
|
|
|
|
onError: (text: string) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const pages = viewPages(p.pages);
|
|
|
|
|
const [picked, setPicked] = useState<{ n: number; onPurpose: boolean } | null>(null);
|
|
|
|
|
const page = landingPage(pages, picked?.n ?? null, picked?.onPurpose ?? false);
|
|
|
|
|
const [marking, setMarking] = useState(false);
|
|
|
|
|
const [sheet, setSheet] = useState<{ weldNo: number | null; at: NewWeldAt | null } | null>(null);
|
|
|
|
|
const serial = useSerial(p, write);
|
|
|
|
|
const { people } = usePeople(p.dept.code);
|
|
|
|
|
const myStamp = people.find((x) => x.id === myId)?.stamp ?? null;
|
|
|
|
|
|
|
|
|
|
if (!page) return <div className="empty">No drawing pages in this packet.</div>;
|
|
|
|
|
|
|
|
|
|
const isVoid = !!page.void;
|
|
|
|
|
const canEdit = p.can.includes("edit_welds");
|
|
|
|
|
const editable = canEdit && !isVoid;
|
|
|
|
|
const onSheet = p.welds.filter((w) => w.page === page.n);
|
|
|
|
|
const current = onSheet.filter((w) => !w.carried);
|
|
|
|
|
const carried = onSheet.filter((w) => w.carried);
|
|
|
|
|
const vText = isVoid ? voidText(page, p.pages) : "";
|
|
|
|
|
const open = sheet ? (sheet.weldNo !== null ? (p.welds.find((w) => w.weld_no === sheet.weldNo) ?? null) : null) : null;
|
|
|
|
|
|
|
|
|
|
const done = async (r: WriteResult): Promise<boolean> => {
|
|
|
|
|
if (r.ok) return true;
|
|
|
|
|
if (r.err !== undefined) {
|
|
|
|
|
if (r.err instanceof ApiError && (r.err.status === 409 || r.err.status === 412)) await reload();
|
|
|
|
|
onError(errorText(r.err));
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const add = async (body: WeldBody) => {
|
|
|
|
|
if (await done(await serial((cur, g) => packetsApi.addWeld(cur.id, body, g)))) {
|
|
|
|
|
setSheet(null);
|
|
|
|
|
setMarking(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const save = async (w: Weld, body: WeldBody) => {
|
|
|
|
|
if (await done(await serial((cur, g) => packetsApi.patchWeld(cur.id, w.weld_no, rv(cur, w.weld_no, w.row_version), body, g)))) setSheet(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const remove = async (w: Weld) => {
|
|
|
|
|
const reason = await askReason({
|
|
|
|
|
title: `Remove weld ${weldId(w)}`,
|
|
|
|
|
sub,
|
|
|
|
|
note: "The weld leaves the weld map. It stays in the record with your reason.",
|
|
|
|
|
min: 3,
|
|
|
|
|
danger: true,
|
|
|
|
|
submitLabel: "Remove weld",
|
|
|
|
|
});
|
|
|
|
|
if (reason === null) return;
|
|
|
|
|
if (await done(await serial((cur, g) => packetsApi.removeWeld(cur.id, w.weld_no, rv(cur, w.weld_no, w.row_version), { reason, ...g })))) setSheet(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const move = (w: Weld, m: MarkMove) =>
|
|
|
|
|
serial((cur, g) =>
|
|
|
|
|
packetsApi.patchWeld(
|
|
|
|
|
cur.id,
|
|
|
|
|
w.weld_no,
|
|
|
|
|
rv(cur, w.weld_no, w.row_version),
|
|
|
|
|
m.bubble ? { bfx: m.bfx, bfy: m.bfy } : { fx: m.fx, fy: m.fy, bfx: m.bfx, bfy: m.bfy },
|
|
|
|
|
g,
|
|
|
|
|
),
|
|
|
|
|
).then(done);
|
|
|
|
|
|
|
|
|
|
const markBtn = editable ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className={marking ? "btn pink" : "btn pri"}
|
|
|
|
|
aria-pressed={marking}
|
|
|
|
|
onClick={() => setMarking((v) => !v)}
|
|
|
|
|
>
|
|
|
|
|
{marking ? "Cancel marking" : "Mark a weld"}
|
|
|
|
|
</button>
|
|
|
|
|
) : null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="stack">
|
|
|
|
|
{isVoid ? (
|
|
|
|
|
<div className="callout warn flush">
|
|
|
|
|
<b>VOID copy, not for fabrication.</b> {vText}. Kept for the record with its weld marks.{" "}
|
|
|
|
|
{page.void?.new_n ? (
|
|
|
|
|
<button className="btn sm" type="button" onClick={() => setPicked({ n: page.void?.new_n ?? page.n, onPurpose: false })}>
|
|
|
|
|
Go to the current sheet
|
|
|
|
|
</button>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
<div className="pagesel">
|
|
|
|
|
{pages.length > 1 ? (
|
|
|
|
|
<div className="chips" role="group" aria-label="Sheets">
|
|
|
|
|
{pages.map((x) => (
|
|
|
|
|
<button
|
|
|
|
|
key={x.n}
|
|
|
|
|
type="button"
|
|
|
|
|
className={`btn sm${x.n === page.n ? " pri" : ""}${x.void ? " voidbtn" : ""}`}
|
|
|
|
|
aria-pressed={x.n === page.n}
|
|
|
|
|
onClick={() => setPicked({ n: x.n, onPurpose: true })}
|
|
|
|
|
>
|
|
|
|
|
{pageLabel(x)}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="mono small muted">{pageLabel(page)}</span>
|
|
|
|
|
)}
|
|
|
|
|
{markBtn}
|
|
|
|
|
{marking ? <span className="small">Tap the weld location on the drawing.</span> : null}
|
|
|
|
|
{!canEdit ? (
|
|
|
|
|
<span className="small muted">
|
|
|
|
|
{p.welds.length} weld{p.welds.length === 1 ? "" : "s"} recorded
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
2026-10-17 23:49:14 -06:00
|
|
|
{!p.removed ? (
|
|
|
|
|
<button className="btn sm" type="button" onClick={() => openPrint(p.id, "map-w", { sheet: page.n })}>
|
|
|
|
|
Print this sheet
|
|
|
|
|
</button>
|
|
|
|
|
) : null}
|
2026-10-16 22:46:56 -06:00
|
|
|
</div>
|
|
|
|
|
<div className="dv">
|
|
|
|
|
<MapViewer
|
|
|
|
|
page={page}
|
|
|
|
|
voidLabel={vText}
|
|
|
|
|
mode={marking && editable ? "marking" : ""}
|
|
|
|
|
label="Weld map"
|
|
|
|
|
onTap={(fx, fy) => {
|
|
|
|
|
if (marking && editable) setSheet({ weldNo: null, at: { page: page.n, fx, fy } });
|
|
|
|
|
}}
|
|
|
|
|
fsBar={
|
|
|
|
|
<>
|
|
|
|
|
<span className="fst">{pageLabel(page)}</span>
|
|
|
|
|
{markBtn}
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{onSheet.map((w) => (
|
|
|
|
|
<WeldMark
|
|
|
|
|
key={w.weld_no}
|
|
|
|
|
weld={w}
|
|
|
|
|
editable={editable && !w.carried}
|
|
|
|
|
onOpen={(x) => setSheet({ weldNo: x.weld_no, at: null })}
|
|
|
|
|
onMove={move}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</MapViewer>
|
|
|
|
|
<div className="side">
|
|
|
|
|
<div className="panel">
|
|
|
|
|
<h3>
|
|
|
|
|
Welds on this sheet ({current.length})
|
|
|
|
|
{current.length !== p.welds.filter((w) => !w.carried).length ? <span className="small muted"> of {p.welds.filter((w) => !w.carried).length}</span> : null}
|
|
|
|
|
</h3>
|
|
|
|
|
{editable && current.length ? <div className="small muted mb8">Drag a bubble to move it off the linework. Drag the arrow to fix the weld location.</div> : null}
|
|
|
|
|
<div className="hlist">
|
|
|
|
|
{current.length ? (
|
|
|
|
|
current.map((w) => <WeldRow key={w.weld_no} w={w} onOpen={(x) => setSheet({ weldNo: x.weld_no, at: null })} />)
|
|
|
|
|
) : (
|
|
|
|
|
<div className="small muted">{editable ? "Tap Mark a weld, then tap the joint on the drawing." : "No welds recorded yet."}</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{carried.length ? (
|
|
|
|
|
<div className="panel">
|
|
|
|
|
<h3>Carried from an earlier department</h3>
|
|
|
|
|
<div className="hlist">
|
|
|
|
|
{carried.map((w) => (
|
|
|
|
|
<WeldRow key={w.weld_no} w={w} onOpen={(x) => setSheet({ weldNo: x.weld_no, at: null })} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
<WeldRecord p={p} mutate={mutate} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{sheet && (sheet.weldNo === null || open) ? (
|
|
|
|
|
<WeldSheet
|
|
|
|
|
key={sheet.weldNo ?? "new"}
|
|
|
|
|
p={p}
|
|
|
|
|
weld={open}
|
|
|
|
|
at={sheet.at}
|
|
|
|
|
editable={canEdit && !(open ? !!p.pages.find((x) => x.n === open.page)?.void : isVoid) && !open?.carried}
|
|
|
|
|
myStamp={myStamp}
|
|
|
|
|
sub={sub}
|
|
|
|
|
onSave={(body) => void (open ? save(open, body) : add(body))}
|
|
|
|
|
onRemove={() => {
|
|
|
|
|
if (open) void remove(open);
|
|
|
|
|
}}
|
|
|
|
|
onClose={() => setSheet(null)}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|