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) => (
))}
{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."}"
)}
);
}