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["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 (
{p.labor ? (
VISUAL tickets {Number(p.labor.hours || 0).toFixed(1)} h
) : null}
); } /** 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( p.station?.id ?? stations[0]?.id ?? null, ); const [q, setQ] = useState(""); const [welder, setWelder] = useState(p.welder?.id ?? ""); const [stamp, setStamp] = useState(p.welder?.stamp ?? ""); const [error, setError] = useState(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 (
The packet appears on that station's screen right away.
{peopleError ? (
{peopleError}
) : null} {loaded && !allowed.length ? (
No one in this department may work at this station.
) : null} {error ? (
{error}
) : null}
); } /** 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 (
{why ?? (!can && reason ?
{reason}
: null)}
); } 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 (
{rejected ? ( <> Rejected by Q.C. Return it to the station for repair, or fix it and send it back to Q.C. ) : ( <> Submitted by {p.station?.name ?? "the station"}. Check the weld map, checklist and heats. When the data book package and the spool are ready, send it to Q.C. )}
{has(p, "send_to_qc") ? (
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.
) : null} {has(p, "return_to_station") ? (
void returnIt()} > Return to station
) : null} {has(p, "route_from_review") ? (
void step( "route_from_review", { dept: routeTo }, { sign: SIGN_ROUTE, ok: "Sent to the department." }, ) } > Send to department
) : null}
); } type AskReason = (opts: ReasonOptions) => Promise; /** * 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 (
{p.return_note ? ( <> Returned. Assign it again when it is ready. ) : ( <> Assign this packet to a station and welder. It appears on that station's screen right away. )}
setAssign(true)} > Send to station 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 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
{assign ? ( 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}
); } 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 (
{stall && st === "assigned" ? ( <> Ready to start. Clock on to the work order in VISUAL (Work orders tab) and start the timer. ) : stall && st === "in_progress" ? ( miss.length ? ( Before submitting:{" "} {miss.map((m, i) => ( {i ? " · " : ""} ))} ) : ( <> Everything is filled in. Submit when the parts are done. ) ) : ( <> In {p.station?.name ?? "a station"} with{" "} {p.welder?.display_name ?? "-"}. You can edit the heat map, weld map, materials and checklist while it is out. )}
{other.length && stall && st === "in_progress" ? (
{other.join(" ")}
) : null}
{stall && st === "assigned" ? ( void step("start", {}, { ok: "Work started." })} > Start work ) : null} {stall && st === "in_progress" && p.can.includes("clock") ? (
) : null} {stall && st === "in_progress" ? ( } onClick={() => void step( "submit", {}, { sign: SIGN_SUBMIT, ok: "Submitted to the supervisor." }, ) } > Submit to supervisor ) : null} void step( st === "assigned" ? "unassign" : "pull_back", {}, { ok: "Pulled back to the queue." }, ) } > Pull back to queue
); } if (st === "review") { if ( has(p, "send_to_qc") || has(p, "return_to_station") || has(p, "route_from_review") ) { return ( ); } if (level && level !== "supervisor") { return (
Submitted by {p.station?.name ?? "the station"}. A supervisor for {deptLabel} reviews it and sends it on.
); } } return null; }