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
+25 -4
View File
@@ -15,6 +15,8 @@ export function ProductDetail({
onMarkGone,
onAudit,
onEdit,
onCheckout,
onCheckin,
}: {
item: Item;
data: Bootstrap;
@@ -23,6 +25,8 @@ export function ProductDetail({
onMarkGone: (i: Item) => void;
onAudit: (i: Item) => void;
onEdit: (i: Item) => void;
onCheckout: (i: Item) => void;
onCheckin: (i: Item) => void;
}) {
const bin = data.bins.find((b) => b.id === item.binId);
const cfg = TYPES.find((t) => t.id === item.type);
@@ -34,6 +38,7 @@ export function ProductDetail({
const sinceCheck = helpers.daysSinceCheck(item, TODAY_STR);
const isActive = item.status === "active";
const isCheckedOut = item.status === "checked-out";
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -58,7 +63,7 @@ export function ProductDetail({
["Shop", helpers.shopName(data, item.shopId)],
["Total cannabinoids", `${item.totalCannabinoids.toFixed(1)}%`],
["Purchase date", fmt.date(item.purchaseDate)],
["Bin", bin ? bin.name : <span style={{ color: "var(--ink-3)" }}></span>],
["Bin", isCheckedOut ? "In your custody" : bin ? bin.name : <span style={{ color: "var(--ink-3)" }}></span>],
["Audit cadence", `Every ${cfg?.cadenceDays ?? "—"} days · ${cfg?.auditMode ?? "—"}`],
[
"Cost per gram",
@@ -69,6 +74,9 @@ export function ProductDetail({
: "—",
],
];
if (item.status === "checked-out") {
detailRows.push(["Checked out", fmt.date(item.checkoutDate)]);
}
if (item.status === "consumed") {
detailRows.push(
["Date finished", fmt.date(item.consumedDate)],
@@ -130,17 +138,27 @@ export function ProductDetail({
Inventory · <span className="mono">{item.assetId}</span>
</div>
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "flex-end" }}>
{isActive && (
<Btn variant="ghost" icon="pocket" onClick={() => onCheckout(item)}>
Check out
</Btn>
)}
{isActive && (
<Btn variant="ghost" icon="check" onClick={() => onAudit(item)}>
Audit
</Btn>
)}
{isActive && (
{isCheckedOut && (
<Btn variant="sage" icon="check" onClick={() => onCheckin(item)}>
Check in
</Btn>
)}
{(isActive || isCheckedOut) && (
<Btn variant="secondary" icon="check" onClick={() => onConsume(item)}>
Mark consumed
</Btn>
)}
{isActive && (
{(isActive || isCheckedOut) && (
<Btn variant="ghost" icon="bin" onClick={() => onMarkGone(item)} />
)}
<Btn variant="ghost" icon="edit" onClick={() => onEdit(item)} />
@@ -159,6 +177,9 @@ export function ProductDetail({
{item.status === "gone" && (
<Pill tone="amber">Gone · {fmt.daysAgo(item.goneDate)}</Pill>
)}
{isCheckedOut && (
<Pill tone="outline">Checked out · {fmt.daysAgo(item.checkoutDate)}</Pill>
)}
{isActive && overdue && <Pill tone="amber">Audit overdue · {sinceCheck}d</Pill>}
</div>
<h1
@@ -214,7 +235,7 @@ export function ProductDetail({
))}
</div>
{isActive && (
{(isActive || isCheckedOut) && (
<div style={{ marginTop: 20 }}>
<div
style={{