import type { Bootstrap, Item, Product } from "../types.js"; import { TYPES, helpers, TODAY_STR } from "../types.js"; import { fmt, TYPE_GLYPHS } from "../format.js"; import { Btn, Pill, Icon } from "./primitives/index.js"; // Right-side drawer for an inventory instance. Shows the asset id and // product context up top, then per-batch fields (price, THC, weight), // audit history, and full detail rows. export function ProductDetail({ item, data, onClose, onConsume, onMarkGone, onAudit, onEdit, onEditProduct, }: { item: Item; data: Bootstrap; onClose: () => void; onConsume: (i: Item) => void; onMarkGone: (i: Item) => void; onAudit: (i: Item) => void; onEdit: (i: Item) => void; onEditProduct: (p: Product) => void; }) { const bin = data.bins.find((b) => b.id === item.binId); const cfg = TYPES.find((t) => t.id === item.type); const product = data.products.find((p) => p.id === item.productId); const pctRemaining = helpers.pctRemaining(item, TODAY_STR); const est = helpers.estimatedRemaining(item, TODAY_STR); const last = helpers.lastAudit(item); const overdue = helpers.auditOverdue(item, TODAY_STR); const sinceCheck = helpers.daysSinceCheck(item, TODAY_STR); const isActive = item.status === "active"; // Sibling instances of the same product (excluding this one) — useful for // seeing previous purchases of the same SKU. const siblings = data.inventoryItems.filter( (i) => i.productId === item.productId && i.id !== item.id, ); const detailRows: [string, React.ReactNode][] = [ ["Asset id", {item.assetId}], ["SKU", {item.sku}], ["Type", `${item.type} · ${item.kind}`], ["Strain", item.strainName ?? Unlinked], ["Brand", helpers.brandName(data, item.brandId)], ["Shop", helpers.shopName(data, item.shopId)], ["Total cannabinoids", `${item.totalCannabinoids.toFixed(1)}%`], ["Purchase date", fmt.date(item.purchaseDate)], ["Bin", bin ? bin.name : ], ["Audit cadence", `Every ${cfg?.cadenceDays ?? "—"} days · ${cfg?.auditMode ?? "—"}`], [ "Cost per gram", item.kind === "bulk" && item.weight > 0 ? fmt.money(item.price / item.weight) : item.kind === "discrete" && item.unitWeight > 0 ? `${fmt.money(item.price / (item.countOriginal * item.unitWeight))} (effective)` : "—", ], ]; if (item.status === "consumed") { detailRows.push( ["Date finished", fmt.date(item.consumedDate)], [ "Lasted", `${Math.round((+new Date(item.consumedDate!) - +new Date(item.purchaseDate)) / 86_400_000)} days`, ], ); } if (item.status === "gone") { detailRows.push( ["Date gone", fmt.date(item.goneDate)], [ "After", `${Math.round((+new Date(item.goneDate!) - +new Date(item.purchaseDate)) / 86_400_000)} days`, ], ); } return (
e.stopPropagation()} style={{ width: "min(720px, 100vw)", height: "100%", background: "var(--bg)", borderLeft: "1px solid var(--line)", overflow: "auto", boxShadow: "var(--shadow-lg)", }} >
Inventory · {item.assetId}
{isActive && ( onAudit(item)}> Audit )} {isActive && ( onConsume(item)}> Mark consumed )} {isActive && ( onMarkGone(item)}> Mark gone )} onEdit(item)}> Edit
{TYPE_GLYPHS[item.type]} {item.type}
{item.status === "consumed" && ( Consumed · {fmt.daysAgo(item.consumedDate)} )} {item.status === "gone" && ( Gone · {fmt.daysAgo(item.goneDate)} )} {isActive && overdue && Audit overdue · {sinceCheck}d}

{item.name}

{helpers.brandName(data, item.brandId)} · from {helpers.shopName(data, item.shopId)}
{product && (
{siblings.length > 0 && ( · {siblings.length} other instance{siblings.length === 1 ? "" : "s"} on file )}
)}
{( [ [ "Price", item.kind === "discrete" && item.countOriginal > 0 ? ( <> {fmt.money(item.price / item.countOriginal)} /unit
{fmt.money(item.price)} total
) : ( fmt.money(item.price) ), ], [ item.kind === "discrete" ? "Quantity" : "Size", item.kind === "discrete" ? `${item.countOriginal} ${cfg?.unit ?? "ct"}` : `${item.weight} ${cfg?.unit ?? "g"}`, ], ["THC", `${item.thc.toFixed(1)}%`], ["CBD", `${item.cbd.toFixed(1)}%`], ] as [string, React.ReactNode][] ).map(([l, v], i) => (
{l}
{v}
))}
{isActive && (
{item.kind === "discrete" ? "Units remaining" : "Estimated remaining"}
{item.kind === "discrete" ? `${item.countLastAudit ?? item.countOriginal} of ${item.countOriginal}` : `${est.toFixed(2)} of ${item.weight} ${cfg?.unit ?? "g"}`} {Math.round(pctRemaining * 100)}%
{item.kind === "bulk" && last && (
Estimated by linear decay since last {last.mode} on {fmt.dateShort(last.date)} ({last.value} {cfg?.unit}). Re-audit to update.
)}
)}
Audit history
{isActive && ( )}
{item.audits.length === 0 ? (
No audits recorded. Cadence for {item.type}: every {cfg?.cadenceDays ?? "—"} days.
) : (
{[...item.audits].reverse().map((a, idx, arr) => (
{a.mode === "weigh" && "Weighed"} {a.mode === "estimate" && "Estimated"} {a.mode === "presence" && (a.confirmedBy === "lost" ? "Marked lost" : "Confirmed presence")}
{fmt.date(a.date)} · {fmt.daysAgo(a.date)}
{a.value} {cfg?.unit}
was {a.prev} {cfg?.unit}
))}
)}
Details
{detailRows.map(([l, v], i) => (
{l} {v}
))}
{(item.status === "consumed" || item.status === "gone") && (
{item.status === "gone" ? "Why it's gone" : "Final notes"}
{item.status === "consumed" && (
{[1, 2, 3, 4, 5].map((n) => ( ))}
)}
"{item.notes ?? "No notes recorded."}"
)}
); }