/** The guided queue — one task at a time, scanned and confirmed, no skipping. */
import { useState } from "react";
import { Text } from "@lotics/ui/text";
import { colors, solid } from "@lotics/ui/colors";
import { Accordion, AccordionContent, AccordionHeader, AccordionMeta, AccordionTitle } from "@lotics/ui/accordion";
import { Status } from "@lotics/ui/status";
import { Box } from "@lotics/ui/box";
import { Button } from "@lotics/ui/button";
import { ChoiceStrip } from "@lotics/ui/choice_strip";
import { NumberInput } from "@lotics/ui/number_input";
import { Divider } from "@lotics/ui/divider";
import { Popover, PopoverContent, PopoverFooter, PopoverTrigger } from "@lotics/ui/popover";
import { Progress } from "@lotics/ui/progress";
import { VerifyField } from "@lotics/ui/verify_field";
import { ScrollArea } from "@lotics/ui/scroll_area";
import { Stack } from "@lotics/ui/stack";
import { Stepper, Step, type StepStatus } from "@lotics/ui/stepper";
import { RegionState } from "@lotics/ui/region_state";
// ─────────────────────────────────────────────────────────────────────────────
// Template, Guided queue — a warehouse PICK RUN. The work is a SEQUENCE of
// physical tasks, not a list to browse: the run hands the operator the next bin,
// they scan to verify they're at the right shelf, confirm the count, and the
// next task takes its place. A single focus column — the CURRENT task is the only
// thing in view (where to walk, what to take, how many); the whole PICK PATH is a
// COLLAPSIBLE section below, collapsed by default, so position is one tap away and
// never competes with the task for the screen. Scan-to-verify, can't-skip
// ordering, and a short-pick exception that flags-and-advances without stalling.
// ─────────────────────────────────────────────────────────────────────────────
interface PickLine {
id: string;
zone: string;
bin: string;
sku: string;
item: string;
qty: number;
status: "pending" | "picked" | "short";
/** Actual units taken — = qty when picked, < qty when short. */
picked?: number;
}
// Ordered by pick path — the run walks the operator down the aisles in sequence.
const WAVE: PickLine[] = [
{ id: "L1", zone: "Aisle A", bin: "A-12-03", sku: "RM-0012", item: "Kraft paper 175gsm, 1.6m", qty: 8, status: "pending" },
{ id: "L2", zone: "Aisle A", bin: "A-14-08", sku: "RM-0018", item: "Medium paper 115gsm, 1.4m", qty: 12, status: "pending" },
{ id: "L3", zone: "Aisle B", bin: "B-03-01", sku: "FG-0203", item: "Carton box 600×400×400, 5-ply", qty: 50, status: "pending" },
{ id: "L4", zone: "Aisle B", bin: "B-05-07", sku: "FG-0218", item: "Carton box 350×250×200, 3-ply", qty: 40, status: "pending" },
{ id: "L5", zone: "Aisle C", bin: "C-01-02", sku: "SUP-0007", item: "Clear packing tape 48mm", qty: 24, status: "pending" },
{ id: "L6", zone: "Aisle C", bin: "C-02-09", sku: "SUP-0011", item: "PP strapping 12mm, 10kg roll", qty: 6, status: "pending" },
{ id: "L7", zone: "Aisle C", bin: "C-04-01", sku: "FG-0226", item: "Offset box 250×180×90, 4-color", qty: 30, status: "pending" },
];
const SHORT_REASONS = [
{ label: "Out of stock", value: "oos" },
{ label: "Damaged", value: "damaged" },
{ label: "Location empty", value: "empty" },
];
// The exception branch — record fewer units than asked + a reason, then advance.
// A short never blocks the run; the shortfall is flagged for backfill.
function ShortGate({ line, onShort }: { line: PickLine; onShort: (actual: number) => void }) {
const [open, setOpen] = useState(false);
const [actual, setActual] = useState(0);
const [reason, setReason] = useState("oos");
return (
{`Short pick: ${line.bin}`}
Record what you actually took — the rest is flagged for backfill.
{`Picked (of ${line.qty})`}
setActual(next ?? 0)} accessibilityLabel="units actually picked" />
);
}
// The hero — the one task in front of the operator right now.
function CurrentTask({ line, index, total, onConfirm, onShort }: {
line: PickLine; index: number; total: number; onConfirm: () => void; onShort: (actual: number) => void;
}) {
const [scan, setScan] = useState("");
const matched = scan.trim().toUpperCase() === line.bin.toUpperCase();
return (
{`Line ${index + 1} of ${total}`}
{/* the bin is the biggest thing on screen — it's where you walk */}
Go to bin
{line.bin}
{line.item}
{line.sku}
Pick
{line.qty}
units
{/* scan-to-verify — confirm you're at the right shelf before the count */}
{ if (matched) onConfirm(); }}
accessibilityLabel="Scan bin to verify location"
/>
);
}
function Complete({ lines, onReset }: { lines: PickLine[]; onReset: () => void }) {
const shorts = lines.filter((l) => l.status === "short").length;
const units = lines.reduce((s, l) => s + (l.status === "short" ? l.picked ?? 0 : l.qty), 0);
return (
0 ? `, ${shorts} short` : ""}`}
>
);
}
// The whole run at a glance: done, now, up next. Read-only — the run is
// sequential, so position is shown, not chosen. Lives in a collapsed disclosure
// below the task so the current step keeps the focus.
function RunPath({ lines, currentIndex }: { lines: PickLine[]; currentIndex: number }) {
return (
{lines.map((l, i) => {
const status: StepStatus =
l.status === "short" ? "warning" : l.status === "picked" ? "done" : i === currentIndex ? "current" : "upcoming";
return (
{l.bin}
{l.item}
{l.status === "short" ? (
) : (
{`×${l.qty}`}
)}
);
})}
);
}
export function TplPick() {
const [lines, setLines] = useState(WAVE);
const idx = lines.findIndex((l) => l.status === "pending");
const current = idx >= 0 ? lines[idx] : null;
const done = lines.filter((l) => l.status !== "pending");
const totalUnits = lines.reduce((s, l) => s + l.qty, 0);
const pickedUnits = lines.reduce((s, l) => s + (l.status === "picked" ? l.qty : l.status === "short" ? l.picked ?? 0 : 0), 0);
const shorts = lines.filter((l) => l.status === "short").length;
const setStatus = (id: string, status: PickLine["status"], picked: number) =>
setLines((prev) => prev.map((l) => (l.id === id ? { ...l, status, picked } : l)));
return (
{/* header — the wave + the cart it's filling */}
Queue W-0142
7 lines for 3 outbound orders on Cart C-07
{/* run progress — the one number that says how close to done */}
{`${done.length} of ${lines.length} lines, ${pickedUnits} of ${totalUnits} units`}
{shorts > 0 ? {`${shorts} short`} : null}
{/* single focus — the current task is the only thing in view */}
{current ? (
setStatus(current.id, "picked", current.qty)}
onShort={(actual) => setStatus(current.id, actual >= current.qty ? "picked" : "short", actual)}
/>
) : (
setLines(WAVE)} />
)}
{/* the whole route, secondary — a collapsed disclosure under the task,
never a persistent side panel */}
Pick path
{`${done.length} of ${lines.length} done`}
);
}