Files
PacketStation/frontend/src/packet/WeldMapTab.tsx
T

244 lines
8.8 KiB
TypeScript
Raw Normal View History

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";
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}
</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>
);
}