743 lines
21 KiB
TypeScript
743 lines
21 KiB
TypeScript
import { useState, type FormEvent, type ReactNode } from "react";
|
|||
|
|
import type { PacketRead } from "../api/types";
|
||
|
|
import { packetsApi } from "../api/packets";
|
||
|
|
import { useAuth } from "../auth/useAuth";
|
||
|
|
import type { ReasonOptions } from "../components/ReasonSheet";
|
||
|
|
import { Sheet } from "../components/Sheet";
|
||
|
|
import { fmtClock, isRunning, liveElapsed, useTicker } from "../lib/clock";
|
||
|
|
import { targetHours } from "../lib/packets";
|
||
|
|
import { useShopDepartments } from "../lib/useDepartments";
|
||
|
|
import {
|
||
|
|
makeStep,
|
||
|
|
shopLevel,
|
||
|
|
stallMissing,
|
||
|
|
usePeople,
|
||
|
|
type ShopTab,
|
||
|
|
type StepFn,
|
||
|
|
} from "./shopUtil";
|
||
|
|
import type { Mutate } from "./tabs";
|
||
|
|
|
||
|
|
/** Signature meanings, as the rules state them. */
|
||
|
|
export const SIGN_SUBMIT = "Submit work to the supervisor";
|
||
|
|
export const SIGN_SEND_TO_QC = "Send to Q.C.";
|
||
|
|
export const SIGN_ROUTE = "Route to another department";
|
||
|
|
|
||
|
|
/** The server's precondition texts the stall shows as links instead. */
|
||
|
|
const STALL_PRECONDITIONS = new Set([
|
||
|
|
"Finish the checklist first.",
|
||
|
|
"Record at least one weld first.",
|
||
|
|
"Enter the filler metal first.",
|
||
|
|
]);
|
||
|
|
|
||
|
|
function stationsOf(
|
||
|
|
depts: ReturnType<typeof useAuth>["departments"],
|
||
|
|
code: string,
|
||
|
|
): Array<{ id: number; name: string }> {
|
||
|
|
const d = depts.find((x) => x.code === code);
|
||
|
|
const out: Array<{ id: number; name: string }> = [];
|
||
|
|
for (const s of d?.stations ?? []) {
|
||
|
|
if (typeof s === "object" && s.id !== undefined && s.active !== false)
|
||
|
|
out.push({ id: s.id, name: s.name });
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The clock line: HH:MM:SS, running or paused, the target, and the VISUAL ticket hours beside it. */
|
||
|
|
export function ClockLine({ p }: { p: PacketRead }) {
|
||
|
|
const clock = p.stage?.clock ?? null;
|
||
|
|
const running = isRunning(clock);
|
||
|
|
const key = `${p.id}:${p.version}:${clock?.elapsed_ms ?? 0}:${clock?.running_since ?? ""}`;
|
||
|
|
const [fetched, setFetched] = useState(() => ({ key, at: Date.now() }));
|
||
|
|
if (fetched.key !== key) setFetched({ key, at: Date.now() });
|
||
|
|
const now = useTicker(running);
|
||
|
|
const ms = liveElapsed(clock, fetched.key === key ? fetched.at : now, now);
|
||
|
|
const target = p.target_hrs ?? targetHours(p);
|
||
|
|
const state = running
|
||
|
|
? "running"
|
||
|
|
: p.status === "in_progress"
|
||
|
|
? "paused"
|
||
|
|
: "";
|
||
|
|
return (
|
||
|
|
<div className="clockrow">
|
||
|
|
<div className="clock" data-testid="clock" aria-live="off">
|
||
|
|
<span
|
||
|
|
className={`cdot${running ? " run" : ""}`}
|
||
|
|
title={running ? "Running" : "Stopped"}
|
||
|
|
aria-hidden="true"
|
||
|
|
/>
|
||
|
|
{fmtClock(ms)}
|
||
|
|
<small>
|
||
|
|
{state ? ` ${state}` : ""} · target {Number(target || 0).toFixed(1)} h
|
||
|
|
</small>
|
||
|
|
</div>
|
||
|
|
{p.labor ? (
|
||
|
|
<div
|
||
|
|
className="labor small"
|
||
|
|
data-testid="labor"
|
||
|
|
title={
|
||
|
|
p.labor.observed_at
|
||
|
|
? `From VISUAL, read ${p.labor.observed_at}`
|
||
|
|
: "From VISUAL"
|
||
|
|
}
|
||
|
|
>
|
||
|
|
VISUAL tickets <b>{Number(p.labor.hours || 0).toFixed(1)} h</b>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The supervisor's assign sheet: station, welder (with search) and stamp. */
|
||
|
|
export function AssignSheet({
|
||
|
|
p,
|
||
|
|
sub,
|
||
|
|
onCancel,
|
||
|
|
onSend,
|
||
|
|
}: {
|
||
|
|
p: PacketRead;
|
||
|
|
sub: string;
|
||
|
|
onCancel: () => void;
|
||
|
|
onSend: (stationId: number, welderId: string, stamp: string) => void;
|
||
|
|
}) {
|
||
|
|
const { departments } = useAuth();
|
||
|
|
const stations = stationsOf(departments, p.dept.code);
|
||
|
|
const { people, loaded, error: peopleError } = usePeople(p.dept.code);
|
||
|
|
const [station, setStation] = useState<number | null>(
|
||
|
|
p.station?.id ?? stations[0]?.id ?? null,
|
||
|
|
);
|
||
|
|
const [q, setQ] = useState("");
|
||
|
|
const [welder, setWelder] = useState<string>(p.welder?.id ?? "");
|
||
|
|
const [stamp, setStamp] = useState<string>(p.welder?.stamp ?? "");
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
const st = stations.find((s) => s.id === station) ?? stations[0];
|
||
|
|
const allowed = people.filter(
|
||
|
|
(u) =>
|
||
|
|
u.level !== "worker" ||
|
||
|
|
!u.station_limits.length ||
|
||
|
|
(st ? u.station_limits.includes(st.id) : true),
|
||
|
|
);
|
||
|
|
const needle = q.trim().toLowerCase();
|
||
|
|
const shown = allowed.filter(
|
||
|
|
(u) =>
|
||
|
|
!needle ||
|
||
|
|
u.display_name.toLowerCase().includes(needle) ||
|
||
|
|
(u.stamp ?? "").toLowerCase().includes(needle) ||
|
||
|
|
u.id === welder,
|
||
|
|
);
|
||
|
|
|
||
|
|
const pick = (id: string) => {
|
||
|
|
setWelder(id);
|
||
|
|
const u = people.find((x) => x.id === id);
|
||
|
|
setStamp(u?.stamp ?? "");
|
||
|
|
setError(null);
|
||
|
|
};
|
||
|
|
|
||
|
|
const submit = (e: FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
if (!st) {
|
||
|
|
setError("This department has no stations. Ask an admin to add one.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!welder || !allowed.some((u) => u.id === welder)) {
|
||
|
|
setError("Pick a welder in this department.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
onSend(st.id, welder, stamp.trim());
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Sheet title="Send to station" sub={sub} onClose={onCancel}>
|
||
|
|
<div className="small">
|
||
|
|
The packet appears on that station's screen right away.
|
||
|
|
</div>
|
||
|
|
<form className="stack" onSubmit={submit} noValidate aria-label="Assign">
|
||
|
|
<label className="f">
|
||
|
|
Station
|
||
|
|
<select
|
||
|
|
aria-label="Station"
|
||
|
|
className="in"
|
||
|
|
value={st?.id ?? ""}
|
||
|
|
onChange={(e) => setStation(Number(e.target.value))}
|
||
|
|
>
|
||
|
|
{stations.map((s) => (
|
||
|
|
<option key={s.id} value={s.id}>
|
||
|
|
{s.name}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</label>
|
||
|
|
<label className="f">
|
||
|
|
Find a welder
|
||
|
|
<input
|
||
|
|
aria-label="Find a welder"
|
||
|
|
className="in"
|
||
|
|
type="search"
|
||
|
|
value={q}
|
||
|
|
placeholder="Name or stamp"
|
||
|
|
onChange={(e) => setQ(e.target.value)}
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
<label className="f">
|
||
|
|
Welder
|
||
|
|
<select
|
||
|
|
aria-label="Welder"
|
||
|
|
className="in"
|
||
|
|
value={welder}
|
||
|
|
onChange={(e) => pick(e.target.value)}
|
||
|
|
>
|
||
|
|
<option value="">{loaded ? "Choose" : "Loading"}</option>
|
||
|
|
{shown.map((u) => (
|
||
|
|
<option key={u.id} value={u.id}>
|
||
|
|
{u.display_name}
|
||
|
|
{u.stamp ? ` [${u.stamp}]` : ""}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</label>
|
||
|
|
{peopleError ? (
|
||
|
|
<div className="small pinktxt">{peopleError}</div>
|
||
|
|
) : null}
|
||
|
|
{loaded && !allowed.length ? (
|
||
|
|
<div className="small muted">
|
||
|
|
No one in this department may work at this station.
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
<label className="f">
|
||
|
|
Stamp
|
||
|
|
<input
|
||
|
|
aria-label="Stamp"
|
||
|
|
className="in w110"
|
||
|
|
value={stamp}
|
||
|
|
maxLength={20}
|
||
|
|
placeholder="ID"
|
||
|
|
onChange={(e) => setStamp(e.target.value)}
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
{error ? (
|
||
|
|
<div className="callout warn flush" role="alert">
|
||
|
|
{error}
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
<div className="sheet-acts">
|
||
|
|
<button className="btn ghost" type="button" onClick={onCancel}>
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
<button className="btn pri" type="submit">
|
||
|
|
Send to station
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</Sheet>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** One step button: hidden when the user can neither do it nor is blocked from it. */
|
||
|
|
function Step({
|
||
|
|
p,
|
||
|
|
action,
|
||
|
|
busy,
|
||
|
|
onClick,
|
||
|
|
className = "btn",
|
||
|
|
disabled = false,
|
||
|
|
why,
|
||
|
|
children,
|
||
|
|
}: {
|
||
|
|
p: PacketRead;
|
||
|
|
action: string;
|
||
|
|
busy: boolean;
|
||
|
|
onClick: () => void;
|
||
|
|
className?: string;
|
||
|
|
disabled?: boolean;
|
||
|
|
why?: ReactNode;
|
||
|
|
children: ReactNode;
|
||
|
|
}) {
|
||
|
|
const can = p.can.includes(action);
|
||
|
|
const blocked = p.blocked[action];
|
||
|
|
if (!can && !blocked) return null;
|
||
|
|
const reason = blocked?.join(" ") ?? "";
|
||
|
|
return (
|
||
|
|
<div className="act">
|
||
|
|
<button
|
||
|
|
className={className}
|
||
|
|
type="button"
|
||
|
|
disabled={!can || busy || disabled}
|
||
|
|
title={!can ? reason : undefined}
|
||
|
|
onClick={onClick}
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</button>
|
||
|
|
{why ??
|
||
|
|
(!can && reason ? <div className="why small">{reason}</div> : null)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const has = (p: PacketRead, a: string) => p.can.includes(a) || !!p.blocked[a];
|
||
|
|
|
||
|
|
/** The supervisor's review: send to Q.C., return to the station, or skip Q.C. and route on. */
|
||
|
|
function ReviewBox({
|
||
|
|
p,
|
||
|
|
busy,
|
||
|
|
step,
|
||
|
|
askReason,
|
||
|
|
sub,
|
||
|
|
}: {
|
||
|
|
p: PacketRead;
|
||
|
|
busy: boolean;
|
||
|
|
step: StepFn;
|
||
|
|
askReason: AskReason;
|
||
|
|
sub: string;
|
||
|
|
}) {
|
||
|
|
const depts = useShopDepartments().filter((d) => d.code !== p.dept.code);
|
||
|
|
const [book, setBook] = useState(false);
|
||
|
|
const [spool, setSpool] = useState(false);
|
||
|
|
const [loc, setLoc] = useState(
|
||
|
|
String((p.qc as { location?: string } | null)?.location ?? ""),
|
||
|
|
);
|
||
|
|
const [note, setNote] = useState("");
|
||
|
|
const [ret, setRet] = useState("");
|
||
|
|
const [route, setRoute] = useState("");
|
||
|
|
const routeTo = depts.some((d) => d.code === route)
|
||
|
|
? route
|
||
|
|
: (depts[0]?.code ?? "");
|
||
|
|
const rejected = (p.qc as { result?: string } | null)?.result === "rejected";
|
||
|
|
const ready = book && spool && !!loc.trim();
|
||
|
|
|
||
|
|
const returnIt = async () => {
|
||
|
|
let reason = ret.trim();
|
||
|
|
if (reason.length < 8) {
|
||
|
|
const r = await askReason({
|
||
|
|
title: "Return to station",
|
||
|
|
sub,
|
||
|
|
label: "Return note",
|
||
|
|
note: "The station sees this note. Say what needs fixing.",
|
||
|
|
min: 8,
|
||
|
|
submitLabel: "Return to station",
|
||
|
|
});
|
||
|
|
if (r === null) return;
|
||
|
|
reason = r;
|
||
|
|
}
|
||
|
|
if (
|
||
|
|
await step(
|
||
|
|
"return_to_station",
|
||
|
|
{ reason },
|
||
|
|
{ ok: "Returned to the station." },
|
||
|
|
)
|
||
|
|
)
|
||
|
|
setRet("");
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<section className="panel actbox" aria-label="Supervisor review">
|
||
|
|
<div>
|
||
|
|
{rejected ? (
|
||
|
|
<>
|
||
|
|
<b>Rejected by Q.C.</b> Return it to the station for repair, or fix
|
||
|
|
it and send it back to Q.C.
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<b>Submitted by {p.station?.name ?? "the station"}.</b> Check the
|
||
|
|
weld map, checklist and heats. When the data book package and the
|
||
|
|
spool are ready, send it to Q.C.
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{has(p, "send_to_qc") ? (
|
||
|
|
<div className="revrow">
|
||
|
|
<label className="ckl">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
checked={book}
|
||
|
|
onChange={(e) => setBook(e.target.checked)}
|
||
|
|
/>
|
||
|
|
Data book package ready
|
||
|
|
</label>
|
||
|
|
<label className="ckl">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
checked={spool}
|
||
|
|
onChange={(e) => setSpool(e.target.checked)}
|
||
|
|
/>
|
||
|
|
Spool ready for inspection
|
||
|
|
</label>
|
||
|
|
<label className="f">
|
||
|
|
Spool location
|
||
|
|
<input
|
||
|
|
aria-label="Spool location"
|
||
|
|
className="in"
|
||
|
|
value={loc}
|
||
|
|
maxLength={80}
|
||
|
|
placeholder="e.g. Bay 2, rack B"
|
||
|
|
onChange={(e) => setLoc(e.target.value)}
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
<label className="f grow2">
|
||
|
|
Note to Q.C.
|
||
|
|
<input
|
||
|
|
aria-label="Note to Q.C."
|
||
|
|
className="in"
|
||
|
|
value={note}
|
||
|
|
maxLength={500}
|
||
|
|
placeholder="Optional"
|
||
|
|
onChange={(e) => setNote(e.target.value)}
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
<Step
|
||
|
|
p={p}
|
||
|
|
action="send_to_qc"
|
||
|
|
busy={busy}
|
||
|
|
className="btn pri"
|
||
|
|
disabled={!ready}
|
||
|
|
onClick={() =>
|
||
|
|
void step(
|
||
|
|
"send_to_qc",
|
||
|
|
{
|
||
|
|
book: true,
|
||
|
|
spool: true,
|
||
|
|
location: loc.trim(),
|
||
|
|
...(note.trim() ? { note: note.trim() } : {}),
|
||
|
|
},
|
||
|
|
{ sign: SIGN_SEND_TO_QC, ok: "Sent to Q.C." },
|
||
|
|
)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
Send to Q.C.
|
||
|
|
</Step>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
{has(p, "return_to_station") ? (
|
||
|
|
<div className="revrow sep">
|
||
|
|
<label className="f grow2">
|
||
|
|
Return note
|
||
|
|
<input
|
||
|
|
aria-label="Return note"
|
||
|
|
className="in"
|
||
|
|
value={ret}
|
||
|
|
maxLength={500}
|
||
|
|
placeholder="What needs fixing"
|
||
|
|
onChange={(e) => setRet(e.target.value)}
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
<Step
|
||
|
|
p={p}
|
||
|
|
action="return_to_station"
|
||
|
|
busy={busy}
|
||
|
|
className="btn pink"
|
||
|
|
onClick={() => void returnIt()}
|
||
|
|
>
|
||
|
|
Return to station
|
||
|
|
</Step>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
{has(p, "route_from_review") ? (
|
||
|
|
<div className="revrow sep">
|
||
|
|
<label className="f grow2">
|
||
|
|
Skip Q.C. - send to department
|
||
|
|
<select
|
||
|
|
aria-label="Skip Q.C. - send to department"
|
||
|
|
className="in"
|
||
|
|
value={routeTo}
|
||
|
|
onChange={(e) => setRoute(e.target.value)}
|
||
|
|
>
|
||
|
|
{depts.map((d) => (
|
||
|
|
<option key={d.code} value={d.code}>
|
||
|
|
{d.code} {d.name}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</label>
|
||
|
|
<Step
|
||
|
|
p={p}
|
||
|
|
action="route_from_review"
|
||
|
|
busy={busy}
|
||
|
|
disabled={!routeTo}
|
||
|
|
onClick={() =>
|
||
|
|
void step(
|
||
|
|
"route_from_review",
|
||
|
|
{ dept: routeTo },
|
||
|
|
{ sign: SIGN_ROUTE, ok: "Sent to the department." },
|
||
|
|
)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
Send to department
|
||
|
|
</Step>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
type AskReason = (opts: ReasonOptions) => Promise<string | null>;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The shop action bars on the packet page, per status and what `can` allows: assign, pull back, the review, and
|
||
|
|
* the stall's start, pause, resume and submit. The server decides; this only shapes the buttons.
|
||
|
|
*/
|
||
|
|
export function ShopBar({
|
||
|
|
p,
|
||
|
|
busy,
|
||
|
|
mutate,
|
||
|
|
askReason,
|
||
|
|
sub,
|
||
|
|
goTab,
|
||
|
|
}: {
|
||
|
|
p: PacketRead;
|
||
|
|
busy: boolean;
|
||
|
|
mutate: Mutate;
|
||
|
|
askReason: AskReason;
|
||
|
|
sub: string;
|
||
|
|
goTab: (t: ShopTab) => void;
|
||
|
|
}) {
|
||
|
|
const { me } = useAuth();
|
||
|
|
const roles = me?.roles ?? [];
|
||
|
|
const [assign, setAssign] = useState(false);
|
||
|
|
const step = makeStep(p, mutate);
|
||
|
|
const st = p.status;
|
||
|
|
const level = shopLevel(roles, p.dept.code);
|
||
|
|
const deptLabel = `${p.dept.code} ${p.dept.name}`;
|
||
|
|
|
||
|
|
const reasonStep = async (
|
||
|
|
action: string,
|
||
|
|
title: string,
|
||
|
|
note: string,
|
||
|
|
ok: string,
|
||
|
|
) => {
|
||
|
|
const reason = await askReason({
|
||
|
|
title,
|
||
|
|
sub,
|
||
|
|
note,
|
||
|
|
min: 8,
|
||
|
|
submitLabel: title,
|
||
|
|
});
|
||
|
|
if (reason === null) return;
|
||
|
|
await step(action, { reason }, { ok });
|
||
|
|
};
|
||
|
|
|
||
|
|
if (st === "ready") {
|
||
|
|
if (
|
||
|
|
!has(p, "assign") &&
|
||
|
|
!has(p, "send_back_to_warehouse") &&
|
||
|
|
!has(p, "return_to_clerical")
|
||
|
|
)
|
||
|
|
return null;
|
||
|
|
return (
|
||
|
|
<section className="panel actbox" aria-label="Assign">
|
||
|
|
<div>
|
||
|
|
{p.return_note ? (
|
||
|
|
<>
|
||
|
|
<b>Returned.</b> Assign it again when it is ready.
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<b>Assign this packet</b> to a station and welder. It appears on
|
||
|
|
that station's screen right away.
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="acts">
|
||
|
|
<Step
|
||
|
|
p={p}
|
||
|
|
action="assign"
|
||
|
|
busy={busy}
|
||
|
|
className="btn pri"
|
||
|
|
onClick={() => setAssign(true)}
|
||
|
|
>
|
||
|
|
Send to station
|
||
|
|
</Step>
|
||
|
|
<Step
|
||
|
|
p={p}
|
||
|
|
action="send_back_to_warehouse"
|
||
|
|
busy={busy}
|
||
|
|
className="btn ghost"
|
||
|
|
onClick={() =>
|
||
|
|
void reasonStep(
|
||
|
|
"send_back_to_warehouse",
|
||
|
|
"Send back to Warehouse",
|
||
|
|
"The warehouse gets it back for the parts. Say what is missing.",
|
||
|
|
"Sent back to the warehouse.",
|
||
|
|
)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
Send back to Warehouse
|
||
|
|
</Step>
|
||
|
|
<Step
|
||
|
|
p={p}
|
||
|
|
action="return_to_clerical"
|
||
|
|
busy={busy}
|
||
|
|
className="btn ghost"
|
||
|
|
onClick={() =>
|
||
|
|
void reasonStep(
|
||
|
|
"return_to_clerical",
|
||
|
|
"Send back to Clerical",
|
||
|
|
"The packet goes back to Clerical as a draft. Say what needs fixing.",
|
||
|
|
"Sent back to Clerical.",
|
||
|
|
)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
Send back to Clerical
|
||
|
|
</Step>
|
||
|
|
</div>
|
||
|
|
{assign ? (
|
||
|
|
<AssignSheet
|
||
|
|
p={p}
|
||
|
|
sub={sub}
|
||
|
|
onCancel={() => setAssign(false)}
|
||
|
|
onSend={(station_id, welder_id, stamp) => {
|
||
|
|
setAssign(false);
|
||
|
|
void step(
|
||
|
|
"assign",
|
||
|
|
{ station_id, welder_id, ...(stamp ? { stamp } : {}) },
|
||
|
|
{ ok: "Sent to the station." },
|
||
|
|
);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
) : null}
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (st === "assigned" || st === "in_progress") {
|
||
|
|
const sup = has(p, "unassign") || has(p, "pull_back");
|
||
|
|
const stall = has(p, "start") || has(p, "submit");
|
||
|
|
if (!sup && !stall) return null;
|
||
|
|
const running = isRunning(p.stage?.clock);
|
||
|
|
const miss = st === "in_progress" ? stallMissing(p) : [];
|
||
|
|
const other = (p.blocked.submit ?? []).filter(
|
||
|
|
(m) => !STALL_PRECONDITIONS.has(m),
|
||
|
|
);
|
||
|
|
return (
|
||
|
|
<section className="panel actbox" aria-label="Station">
|
||
|
|
<div>
|
||
|
|
{stall && st === "assigned" ? (
|
||
|
|
<>
|
||
|
|
<b>Ready to start.</b> Clock on to the work order in VISUAL (Work
|
||
|
|
orders tab) and start the timer.
|
||
|
|
</>
|
||
|
|
) : stall && st === "in_progress" ? (
|
||
|
|
miss.length ? (
|
||
|
|
<span data-testid="submit-missing">
|
||
|
|
<b>Before submitting:</b>{" "}
|
||
|
|
{miss.map((m, i) => (
|
||
|
|
<span key={m.t}>
|
||
|
|
{i ? " · " : ""}
|
||
|
|
<button
|
||
|
|
className="lnk"
|
||
|
|
type="button"
|
||
|
|
onClick={() => goTab(m.tab)}
|
||
|
|
>
|
||
|
|
{m.t}
|
||
|
|
</button>
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</span>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<b>Everything is filled in.</b> Submit when the parts are done.
|
||
|
|
</>
|
||
|
|
)
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
In <b>{p.station?.name ?? "a station"}</b> with{" "}
|
||
|
|
{p.welder?.display_name ?? "-"}. You can edit the heat map, weld
|
||
|
|
map, materials and checklist while it is out.
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{other.length && stall && st === "in_progress" ? (
|
||
|
|
<div className="why small">{other.join(" ")}</div>
|
||
|
|
) : null}
|
||
|
|
<div className="acts center">
|
||
|
|
<ClockLine p={p} />
|
||
|
|
{stall && st === "assigned" ? (
|
||
|
|
<Step
|
||
|
|
p={p}
|
||
|
|
action="start"
|
||
|
|
busy={busy}
|
||
|
|
className="btn pri"
|
||
|
|
onClick={() => void step("start", {}, { ok: "Work started." })}
|
||
|
|
>
|
||
|
|
Start work
|
||
|
|
</Step>
|
||
|
|
) : null}
|
||
|
|
{stall && st === "in_progress" && p.can.includes("clock") ? (
|
||
|
|
<div className="act">
|
||
|
|
<button
|
||
|
|
className="btn"
|
||
|
|
type="button"
|
||
|
|
disabled={busy}
|
||
|
|
onClick={() =>
|
||
|
|
void mutate((g) =>
|
||
|
|
packetsApi.clock(p.id, running ? "pause" : "resume", g),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
{running ? "Pause" : "Resume"}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
{stall && st === "in_progress" ? (
|
||
|
|
<Step
|
||
|
|
p={p}
|
||
|
|
action="submit"
|
||
|
|
busy={busy}
|
||
|
|
className="btn pri"
|
||
|
|
why={<span />}
|
||
|
|
onClick={() =>
|
||
|
|
void step(
|
||
|
|
"submit",
|
||
|
|
{},
|
||
|
|
{ sign: SIGN_SUBMIT, ok: "Submitted to the supervisor." },
|
||
|
|
)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
Submit to supervisor
|
||
|
|
</Step>
|
||
|
|
) : null}
|
||
|
|
<Step
|
||
|
|
p={p}
|
||
|
|
action={st === "assigned" ? "unassign" : "pull_back"}
|
||
|
|
busy={busy}
|
||
|
|
onClick={() =>
|
||
|
|
void step(
|
||
|
|
st === "assigned" ? "unassign" : "pull_back",
|
||
|
|
{},
|
||
|
|
{ ok: "Pulled back to the queue." },
|
||
|
|
)
|
||
|
|
}
|
||
|
|
>
|
||
|
|
Pull back to queue
|
||
|
|
</Step>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (st === "review") {
|
||
|
|
if (
|
||
|
|
has(p, "send_to_qc") ||
|
||
|
|
has(p, "return_to_station") ||
|
||
|
|
has(p, "route_from_review")
|
||
|
|
) {
|
||
|
|
return (
|
||
|
|
<ReviewBox
|
||
|
|
p={p}
|
||
|
|
busy={busy}
|
||
|
|
step={step}
|
||
|
|
askReason={askReason}
|
||
|
|
sub={sub}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if (level && level !== "supervisor") {
|
||
|
|
return (
|
||
|
|
<div className="callout" role="note" data-testid="review-readonly">
|
||
|
|
<b>Submitted by {p.station?.name ?? "the station"}.</b> A supervisor
|
||
|
|
for {deptLabel} reviews it and sends it on.
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|