Add checkout/custody feature for tracking items in personal possession
Build and push image / build (push) Successful in 1m8s

Items can now be checked out of their bin into "my custody" and later
checked back in or marked consumed. Adds checkout/checkin API endpoints,
a My Custody sidebar page, CheckoutFlow and CheckinFlow modals, and
updates ProductDetail, Inventory, ConsumeFlow, and MarkGoneFlow to
handle the new checked-out status. Bulk items prompt for remaining
weight on check-in.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 20:49:58 -04:00
parent 04bf009a83
commit e7fd9af62c
17 changed files with 689 additions and 18 deletions
+10 -3
View File
@@ -5,7 +5,7 @@ import { remainingShort } from "../stats.js";
import { fmt, TYPE_GLYPHS } from "../format.js";
import { Btn, Card, Pill, Icon, Select, inputStyle } from "../components/primitives/index.js";
type FilterKey = "active" | "consumed" | "gone" | "all";
type FilterKey = "active" | "checked-out" | "consumed" | "gone" | "all";
type SortKey = "recent" | "name" | "thc" | "remaining" | "price" | "audit";
type ViewKey = "flat" | "grouped";
@@ -50,6 +50,7 @@ export function Inventory({
const filtered = useMemo(() => {
let out = items;
if (filter === "active") out = out.filter((i) => i.status === "active");
else if (filter === "checked-out") out = out.filter((i) => i.status === "checked-out");
else if (filter === "consumed") out = out.filter((i) => i.status === "consumed");
else if (filter === "gone") out = out.filter((i) => i.status === "gone");
if (typeFilter !== "all") out = out.filter((i) => i.type === typeFilter);
@@ -139,6 +140,7 @@ export function Inventory({
value={filter}
options={[
["active", "Active"],
["checked-out", "Checked out"],
["consumed", "Consumed"],
["gone", "Gone"],
["all", "All"],
@@ -435,7 +437,7 @@ function ItemRow({
const overdue = helpers.auditOverdue(i, TODAY_STR);
const sinceCheck = helpers.daysSinceCheck(i, TODAY_STR);
const last = helpers.lastAudit(i);
const isInactive = i.status !== "active";
const isInactive = i.status !== "active" && i.status !== "checked-out";
return (
<div
onClick={() => onSelect(i)}
@@ -480,6 +482,9 @@ function ItemRow({
{i.status === "gone" && (
<Pill tone="amber" style={{ marginLeft: 6, fontSize: 10 }}>Gone</Pill>
)}
{i.status === "checked-out" && (
<Pill tone="outline" style={{ marginLeft: 6, fontSize: 10 }}>Checked out</Pill>
)}
{i.status === "active" && overdue && (
<Pill tone="amber" style={{ marginLeft: 6, fontSize: 10 }}>Audit due</Pill>
)}
@@ -531,7 +536,9 @@ function ItemRow({
)}
</div>
<div style={{ fontSize: 12, color: "var(--ink-3)", display: "flex", alignItems: "center", gap: 6 }}>
{bin ? bin.name : <span style={{ fontStyle: "italic" }}></span>}
{i.status === "checked-out" ? (
<span style={{ fontStyle: "italic", color: "var(--sage)" }}>Custody</span>
) : bin ? bin.name : <span style={{ fontStyle: "italic" }}></span>}
<span className="inv-row-chevron" style={{ color: "var(--ink-3)", marginLeft: "auto", fontSize: 14 }}></span>
</div>
</div>