import { useEffect } from "react"; import type { Bootstrap, Brand, Product, Item } from "../types.js"; import { TYPES, helpers, enrichItems } from "../types.js"; import { getToday, getStoredTimezone } from "../tz.js"; import { fmt, TYPE_GLYPHS } from "../format.js"; import { Btn, Pill, Icon } from "./primitives/index.js"; import { remainingShort } from "../stats.js"; import { useExitAnimation } from "../hooks/useExitAnimation.js"; import { useFocusTrap } from "../hooks/useFocusTrap.js"; export function BrandDetail({ brand, data, onClose, onEdit, onDelete, onSelectSku, onSelectItem, backLabel, onBack, }: { brand: Brand; data: Bootstrap; onClose: () => void; onEdit: () => void; onDelete: () => void; onSelectSku: (p: Product) => void; onSelectItem: (i: Item) => void; backLabel?: string; onBack?: () => void; }) { const products = data.products.filter((p) => p.brandId === brand.id); const strainMap = new Map(data.strains.map((s) => [s.id, s])); const allItems = enrichItems(data).filter((i) => i.brandId === brand.id); const hasItems = allItems.length > 0; const active = allItems.filter((i) => i.status === "active" || i.status === "checked-out"); const consumed = allItems.filter((i) => i.status === "consumed"); const gone = allItems.filter((i) => i.status === "gone"); const totalSpend = allItems.reduce((s, i) => s + i.price, 0); const avgPrice = hasItems ? totalSpend / allItems.length : 0; const rated = allItems.filter((i) => i.rating != null); const avgRating = rated.length > 0 ? rated.reduce((s, i) => s + i.rating!, 0) / rated.length : null; const sortedItems = [...allItems].sort( (a, b) => +new Date(b.purchaseDate) - +new Date(a.purchaseDate), ); const recentItems = sortedItems.slice(0, 20); const todayStr = getToday(getStoredTimezone()); const tz = getStoredTimezone(); const { closing, triggerClose } = useExitAnimation(220, onClose); const trapRef = useFocusTrap(); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") triggerClose(); }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [triggerClose]); const statCards: [string, React.ReactNode][] = [ ["SKUs", String(products.length)], ["Purchases", String(allItems.length)], ["Total spent", hasItems ? fmt.money(totalSpend) : "—"], ["Avg price", hasItems ? fmt.money(avgPrice) : "—"], ]; return (
e.stopPropagation()} style={{ width: "min(720px, 100vw)", height: "100%", animation: closing ? "drawer-out 220ms ease-in forwards" : "drawer-in 250ms ease-out", background: "var(--bg)", borderLeft: "1px solid var(--line)", overflow: "auto", boxShadow: "var(--shadow-lg)", }} >
{onBack && backLabel && ( )}
Brand

{brand.name}

{hasItems && (
{statCards.map(([l, v], i) => (
{l}
{v}
))}
)} {hasItems && (
Lifecycle
{active.length > 0 && {active.length} active} {consumed.length > 0 && {consumed.length} consumed} {gone.length > 0 && {gone.length} gone}
)} {avgRating != null && (
Ratings
{[1, 2, 3, 4, 5].map((n) => ( ))}
{avgRating.toFixed(1)} from {rated.length} review{rated.length === 1 ? "" : "s"}
)} {products.length > 0 && (
SKUs ({products.length})
{products.map((p, idx) => { const strain = strainMap.get(p.strainId); const itemCount = allItems.filter((i) => i.productId === p.id).length; return (
onSelectSku(p)} className="inv-row" style={{ padding: "12px 16px", borderBottom: idx < products.length - 1 ? "1px solid var(--line)" : "none", display: "grid", gridTemplateColumns: "24px 1fr auto auto auto", alignItems: "center", gap: 12, background: "var(--surface)", cursor: "pointer", }} > {TYPE_GLYPHS[p.type]}
{strain?.name ?? "(unknown)"}
{p.type} · {p.kind}
{p.sku} {itemCount} item{itemCount === 1 ? "" : "s"}
); })}
)} {hasItems && (
Recent purchases ({allItems.length})
{recentItems.map((item, idx) => { const isInactive = item.status !== "active" && item.status !== "checked-out"; return (
onSelectItem(item)} className="inv-row" style={{ padding: "12px 16px", borderBottom: idx < recentItems.length - 1 ? "1px solid var(--line)" : "none", display: "grid", gridTemplateColumns: "auto 1fr auto auto auto", alignItems: "center", gap: 12, background: "var(--surface)", cursor: "pointer", opacity: isInactive ? 0.55 : 1, }} > {item.assetId} {item.status === "consumed" && Consumed} {item.status === "gone" && Gone} {item.status === "checked-out" && Checked out} {item.status === "active" && helpers.auditOverdue(item, todayStr) && ( Audit due )} {item.status === "active" && !helpers.auditOverdue(item, todayStr) && ( Active )} {fmt.money(item.price)} {fmt.dateShort(item.purchaseDate, tz)} {(item.status === "active" || item.status === "checked-out") ? remainingShort(item) : ""}
); })}
)} {!hasItems && (
No inventory items yet
Products from this brand will appear here.
)} {hasItems && (
Cannot delete this brand while it has associated inventory items.
)}
); }