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
+65 -4
View File
@@ -212,6 +212,67 @@ inventoryRouter.patch("/inventory/:id", (req, res) => {
res.json({ ok: true });
});
inventoryRouter.post("/inventory/:id/checkout", (req, res) => {
const { id } = req.params;
const { date } = req.body as { date: string };
const result = db
.prepare(
`UPDATE inventory_items
SET status = 'checked-out', checkout_date = ?, bin_id = NULL
WHERE id = ? AND status = 'active'`,
)
.run(date, id);
if (result.changes === 0) return res.status(404).json({ error: "not found or not active" });
res.json({ ok: true });
});
inventoryRouter.post("/inventory/:id/checkin", (req, res) => {
const { id } = req.params;
const { date, binId, remainingWeight } = req.body as {
date: string;
binId: string;
remainingWeight?: number;
};
const item = db
.prepare<
[string],
{ product_id: string; last_audit_weight: number | null; weight: number }
>(
`SELECT product_id, last_audit_weight, weight
FROM inventory_items WHERE id = ? AND status = 'checked-out'`,
)
.get(id);
if (!item) return res.status(404).json({ error: "not found or not checked-out" });
const product = db
.prepare<[string], { kind: string }>(`SELECT kind FROM products WHERE id = ?`)
.get(item.product_id);
const isBulk = product?.kind === "bulk";
const tx = db.transaction(() => {
db.prepare(
`UPDATE inventory_items
SET status = 'active', bin_id = ?, checkout_date = NULL
WHERE id = ?`,
).run(binId, id);
if (isBulk && remainingWeight != null && Number.isFinite(remainingWeight)) {
const prev = item.last_audit_weight ?? item.weight;
db.prepare(
`INSERT INTO audits (inventory_id, date, mode, value, prev_value, confirmed_by)
VALUES (?, ?, 'weigh', ?, ?, 'checkin')`,
).run(id, date, remainingWeight, prev);
db.prepare(`UPDATE inventory_items SET last_audit_weight = ? WHERE id = ?`).run(
remainingWeight,
id,
);
}
});
tx();
res.json({ ok: true });
});
inventoryRouter.post("/inventory/:id/finish", (req, res) => {
const { id } = req.params;
const { date, rating, notes } = req.body as {
@@ -222,8 +283,8 @@ inventoryRouter.post("/inventory/:id/finish", (req, res) => {
const result = db
.prepare(
`UPDATE inventory_items
SET status = 'consumed', consumed_date = ?, rating = ?, notes = ?, bin_id = NULL
WHERE id = ? AND status = 'active'`,
SET status = 'consumed', consumed_date = ?, rating = ?, notes = ?, bin_id = NULL, checkout_date = NULL
WHERE id = ? AND status IN ('active', 'checked-out')`,
)
.run(date, rating ?? null, notes ?? null, id);
if (result.changes === 0) return res.status(404).json({ error: "not found or not active" });
@@ -242,8 +303,8 @@ inventoryRouter.post("/inventory/:id/gone", (req, res) => {
const result = db
.prepare(
`UPDATE inventory_items
SET status = 'gone', gone_date = ?, notes = ?, bin_id = NULL
WHERE id = ? AND status = 'active'`,
SET status = 'gone', gone_date = ?, notes = ?, bin_id = NULL, checkout_date = NULL
WHERE id = ? AND status IN ('active', 'checked-out')`,
)
.run(date, combinedNotes, id);
if (result.changes === 0) throw new Error("not found");