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
+169
View File
@@ -0,0 +1,169 @@
import { useMemo } from "react";
import type { Bootstrap, Item } from "../types.js";
import { helpers, TODAY_STR, enrichItems } from "../types.js";
import { remainingShort } from "../stats.js";
import { fmt, TYPE_GLYPHS } from "../format.js";
import { Btn, Card, Icon } from "../components/primitives/index.js";
export function CustodyView({
data,
onSelectItem,
onCheckin,
onConsume,
onMarkGone,
}: {
data: Bootstrap;
onSelectItem: (i: Item) => void;
onCheckin: (i: Item) => void;
onConsume: (i: Item) => void;
onMarkGone: (i: Item) => void;
}) {
const items = useMemo(() => enrichItems(data), [data]);
const checkedOut = useMemo(
() =>
items
.filter((i) => i.status === "checked-out")
.sort(
(a, b) =>
+new Date(b.checkoutDate ?? b.purchaseDate) -
+new Date(a.checkoutDate ?? a.purchaseDate),
),
[items],
);
return (
<div style={{ padding: "40px 48px", maxWidth: 1100 }}>
<div style={{ marginBottom: 32 }}>
<h1
className="serif"
style={{ fontSize: 44, margin: 0, fontWeight: 500, letterSpacing: "-0.02em" }}
>
My Custody
</h1>
<div style={{ fontSize: 13, color: "var(--ink-3)", marginTop: 6 }}>
{checkedOut.length} item{checkedOut.length === 1 ? "" : "s"} checked out
</div>
</div>
{checkedOut.length === 0 ? (
<div
style={{
textAlign: "center",
padding: "80px 20px",
color: "var(--ink-3)",
}}
>
<Icon name="pocket" size={40} color="var(--ink-4)" />
<div
style={{
fontSize: 14,
fontStyle: "italic",
marginTop: 16,
}}
>
Nothing checked out right now.
</div>
</div>
) : (
<Card padded={false}>
<div
style={{
display: "grid",
gridTemplateColumns: "32px 2fr 1fr 0.8fr 0.8fr auto",
padding: "10px 16px",
fontSize: 11,
color: "var(--ink-3)",
textTransform: "uppercase",
letterSpacing: "0.06em",
borderBottom: "1px solid var(--line)",
}}
>
<div />
<div>Item</div>
<div>Brand</div>
<div>Remaining</div>
<div>Checked out</div>
<div />
</div>
{checkedOut.map((item) => (
<CustodyRow
key={item.id}
item={item}
data={data}
onSelect={() => onSelectItem(item)}
onCheckin={() => onCheckin(item)}
onConsume={() => onConsume(item)}
onMarkGone={() => onMarkGone(item)}
/>
))}
</Card>
)}
</div>
);
}
function CustodyRow({
item,
data,
onSelect,
onCheckin,
onConsume,
onMarkGone,
}: {
item: Item;
data: Bootstrap;
onSelect: () => void;
onCheckin: () => void;
onConsume: () => void;
onMarkGone: () => void;
}) {
const glyph = TYPE_GLYPHS[item.type] ?? "·";
const pct = helpers.pctRemaining(item, TODAY_STR);
return (
<div
style={{
display: "grid",
gridTemplateColumns: "32px 2fr 1fr 0.8fr 0.8fr auto",
padding: "12px 16px",
alignItems: "center",
borderBottom: "1px solid var(--line)",
cursor: "pointer",
}}
onClick={onSelect}
>
<div style={{ fontSize: 16, textAlign: "center", opacity: 0.6 }}>{glyph}</div>
<div>
<div style={{ fontWeight: 500, fontSize: 14 }}>{item.name}</div>
<div className="mono" style={{ fontSize: 11, color: "var(--ink-3)" }}>
{item.assetId}
</div>
</div>
<div style={{ fontSize: 13, color: "var(--ink-2)" }}>
{helpers.brandName(data, item.brandId)}
</div>
<div style={{ fontFamily: "var(--mono)", fontSize: 12 }}>
{remainingShort(item)}
<span style={{ color: "var(--ink-3)", marginLeft: 6 }}>
{Math.round(pct * 100)}%
</span>
</div>
<div style={{ fontSize: 12, color: "var(--ink-3)" }}>
{fmt.daysAgo(item.checkoutDate)}
</div>
<div
style={{ display: "flex", gap: 4 }}
onClick={(e) => e.stopPropagation()}
>
<Btn variant="sage" icon="check" onClick={onCheckin}>
Check in
</Btn>
<Btn variant="secondary" icon="check" onClick={onConsume}>
Consume
</Btn>
<Btn variant="ghost" icon="bin" onClick={onMarkGone} />
</div>
</div>
);
}