Files
Apothecary/weed-tracker/project/screens-1.jsx
T

579 lines
32 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Dashboard, Inventory, Product detail screens
const H = window.DATA_HELPERS;
const TODAY_STR = "2026-04-25";
// ─── Helpers shared across screens ─────────────────────────────────
const remainingDisplay = (p) => {
const cfg = window.SAMPLE_DATA.types.find(t => t.id === p.type);
if (p.kind === "discrete") {
const cur = p.countLastAudit != null ? p.countLastAudit : p.countOriginal;
return `${cur} / ${p.countOriginal} ${cfg?.unit || "ct"}`;
}
const est = H.estimatedRemaining(p, TODAY_STR);
return `${est.toFixed(2).replace(/\.?0+$/,"")} / ${p.weight} ${cfg?.unit || "g"}`;
};
const remainingShort = (p) => {
if (p.kind === "discrete") {
const cur = p.countLastAudit != null ? p.countLastAudit : p.countOriginal;
return `${cur} ct`;
}
const cfg = window.SAMPLE_DATA.types.find(t => t.id === p.type);
const est = H.estimatedRemaining(p, TODAY_STR);
return `${est.toFixed(2).replace(/\.?0+$/,"") || "0"} ${cfg?.unit || "g"}`;
};
const Dashboard = ({data, stats, onNav, onSelectProduct, onAudit, onMarkGone}) => {
const series30 = stats.series30.map(d => ({ date: d.date, value: d.grams, label: "" }));
// Type breakdown
const typeColors = {
"Flower": "var(--sage)",
"Concentrate": "var(--terracotta)",
"Edible": "var(--amber)",
"Vaporizer": "var(--plum)",
"Pre-roll": "oklch(50% 0.06 200)",
"Tincture": "oklch(55% 0.06 270)"
};
const segments = Object.entries(stats.typeBreakdown).map(([k, v]) => ({
label: k, value: v, color: typeColors[k] || "var(--ink-3)"
}));
// Sparklines
const last7Series = stats.series7.map(l => l.grams);
const last30Series = series30.map(d => d.value);
const overdue = stats.overdueAudits;
const lowBulk = stats.lowStockBulk;
const lowDiscrete = stats.lowStockDiscreteGroups;
return (
<div style={{padding: "32px 40px 80px", maxWidth: 1400, margin: "0 auto"}}>
{/* Header */}
<div style={{display: "flex", alignItems: "center", justifyContent: "space-between", gap: 24, marginBottom: 16, flexWrap: "wrap"}}>
<div style={{minWidth: 0}}>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>Saturday · April 25, 2026</div>
<h1 className="serif" style={{fontSize: 36, margin: "6px 0 0", fontWeight: 500, letterSpacing: "-0.02em", lineHeight: 1.1, whiteSpace: "nowrap"}}>Good evening.</h1>
</div>
<div style={{display: "flex", gap: 8, flexShrink: 0}}>
<Btn variant="secondary" icon="plus" onClick={() => onNav("add")}>New product</Btn>
<Btn variant="secondary" icon="check" onClick={() => onNav("audit")}>Audit</Btn>
<Btn variant="primary" icon="check" onClick={() => onNav("consume")}>Mark finished</Btn>
</div>
</div>
<div style={{fontSize: 14, color: "var(--ink-2)", marginBottom: 28, maxWidth: 700}}>
{stats.activeCount} active items across {data.bins.length} bins · {stats.consumedCount} consumed · {stats.goneCount} gone.
{overdue.length > 0 && <span style={{color: "var(--terracotta)"}}> · {overdue.length} audit{overdue.length === 1 ? "" : "s"} overdue.</span>}
</div>
{/* Top stats row */}
<div style={{display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 14, marginBottom: 14}}>
<Stat
label="Daily average"
value={stats.dailyAvg.toFixed(2)}
unit="g / day"
sub={`${fmt.g(stats.weeklyAvg)} weekly · ${fmt.g(stats.monthlyAvg)} monthly`}
spark={<Sparkline values={last30Series} width={240} height={28} color="var(--sage)" fill />}
/>
<Stat
label="Avg cost per gram"
value={fmt.money(stats.avgPerGram)}
sub={`Across ${data.products.length} purchases`}
/>
<Stat
label="30-day spend"
value={fmt.moneyShort(stats.spend30)}
sub={`Inventory value: ${fmt.money(stats.inventoryValue)}${stats.goneSpend > 0 ? ` · ${fmt.money(stats.goneSpend)} lost` : ""}`}
/>
<Stat
label="THC last 7 days"
value={stats.thcLast7.toLocaleString()}
unit="mg"
sub={`Last 30: ${(stats.thcLast30/1000).toFixed(1)} g THC`}
spark={<Sparkline values={last7Series} width={240} height={28} color="var(--terracotta)" />}
/>
</div>
{/* Audit alert strip */}
{overdue.length > 0 && (
<Card style={{marginBottom: 14, borderColor: "var(--amber)", background: "var(--amber-soft)"}}>
<div style={{display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap"}}>
<div style={{flex: 1, minWidth: 240}}>
<div className="smallcaps" style={{color: "oklch(48% 0.10 75)"}}>Audit overdue</div>
<div className="serif" style={{fontSize: 20, marginTop: 4, color: "var(--ink)"}}>
{overdue.length} item{overdue.length === 1 ? "" : "s"} haven't been checked in a while
</div>
<div style={{fontSize: 12, color: "var(--ink-2)", marginTop: 4}}>
{overdue.slice(0, 3).map(p => p.name).join(" · ")}
{overdue.length > 3 && ` · +${overdue.length - 3} more`}
</div>
</div>
<Btn variant="secondary" icon="check" onClick={() => onAudit && onAudit(overdue[0])}>Run audit</Btn>
</div>
</Card>
)}
{/* Main grid */}
<div style={{display: "grid", gridTemplateColumns: "2fr 1fr", gap: 14, marginBottom: 14}}>
{/* Consumption chart */}
<Card>
<div style={{display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 18}}>
<div>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>Consumption</div>
<div className="serif" style={{fontSize: 22, marginTop: 4}}>Last 30 days</div>
</div>
<div style={{display: "flex", gap: 16, fontSize: 12, color: "var(--ink-3)"}}>
<div><span style={{color: "var(--ink)"}} className="serif" >{fmt.g(stats.series30.reduce((s,l)=>s+l.grams,0))}</span> est. total</div>
<div><span style={{color: "var(--ink)"}} className="serif">{stats.avgGap.toFixed(0)}</span> day avg between buys</div>
</div>
</div>
<BarChart data={series30.map(d => ({...d, label: ""}))} height={140} color="var(--sage)" />
<div style={{display: "flex", justifyContent: "space-between", marginTop: 8, fontSize: 10, color: "var(--ink-3)", fontFamily: "var(--mono)"}}>
<span>30 days ago</span>
<span>15 days ago</span>
<span>today</span>
</div>
</Card>
{/* Type breakdown */}
<Card>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>By type · grams on hand</div>
<div className="serif" style={{fontSize: 22, marginTop: 4, marginBottom: 16}}>Inventory</div>
<div style={{display: "flex", alignItems: "center", gap: 20}}>
<Donut segments={segments} size={140} thickness={20} />
<div style={{flex: 1, display: "flex", flexDirection: "column", gap: 8}}>
{segments.map(s => (
<div key={s.label} style={{display: "flex", alignItems: "center", gap: 8, fontSize: 12}}>
<div style={{width: 8, height: 8, borderRadius: 2, background: s.color}} />
<div style={{flex: 1, color: "var(--ink-2)"}}>{s.label}</div>
<div className="mono" style={{color: "var(--ink)"}}>{s.value.toFixed(1)}g</div>
</div>
))}
</div>
</div>
</Card>
</div>
<div style={{display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 14, marginBottom: 14}}>
<Stat
label="Days of supply"
value={Math.round(stats.daysOfSupply)}
unit="days"
sub="Flower & pre-rolls at current pace"
/>
<Stat
label="Avg lifespan"
value={Math.round(stats.avgLifespan)}
unit="days"
sub="From purchase to finished"
/>
<Stat
label="Days between buys"
value={stats.avgGap.toFixed(1)}
unit="days"
sub="Average across all purchases"
/>
</div>
{/* Bottom row: shop + brand + low stock */}
<div style={{display: "grid", gridTemplateColumns: "1fr 1fr 1.4fr", gap: 14}}>
<Card>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>Favorite shop</div>
<div className="serif" style={{fontSize: 28, marginTop: 6, fontWeight: 500}}>{stats.favShop[0]}</div>
<div style={{fontSize: 12, color: "var(--ink-3)", marginTop: 4}}>{stats.favShop[1]} of {data.products.length} purchases</div>
</Card>
<Card>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>Favorite brand</div>
<div className="serif" style={{fontSize: 28, marginTop: 6, fontWeight: 500}}>{stats.favBrand[0]}</div>
<div style={{fontSize: 12, color: "var(--ink-3)", marginTop: 4}}>{stats.favBrand[1]} purchases</div>
</Card>
<Card padded={false}>
<div style={{padding: "20px 20px 12px", display: "flex", justifyContent: "space-between", alignItems: "baseline"}}>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>Low stock · running out</div>
<div style={{fontSize: 11, color: "var(--ink-3)"}}>{lowBulk.length + lowDiscrete.length} item{(lowBulk.length + lowDiscrete.length) === 1 ? "" : "s"}</div>
</div>
<div>
{(lowBulk.length + lowDiscrete.length) === 0 && <div style={{padding: "0 20px 20px", fontSize: 13, color: "var(--ink-3)"}}>Nothing running low.</div>}
{lowBulk.slice(0, 3).map(p => {
const pct = H.pctRemaining(p, TODAY_STR);
return (
<div key={p.id} onClick={() => onSelectProduct(p)} style={{padding: "10px 20px", borderTop: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, cursor: "pointer"}}>
<div style={{flex: 1, minWidth: 0}}>
<div style={{fontSize: 13, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}}>{p.name}</div>
<div style={{fontSize: 11, color: "var(--ink-3)"}}>{H.brandName(data, p.brandId)} · {p.type}</div>
</div>
<div style={{width: 60, height: 4, background: "var(--bg-3)", borderRadius: 2, overflow: "hidden"}}>
<div style={{width: `${pct * 100}%`, height: "100%", background: pct < 0.15 ? "var(--terracotta)" : "var(--amber)"}} />
</div>
<div className="mono" style={{fontSize: 11, color: "var(--ink-2)", width: 60, textAlign: "right"}}>{remainingShort(p)}</div>
</div>
);
})}
{lowDiscrete.slice(0, 2).map(g => (
<div key={g.key} onClick={() => onSelectProduct(g.items[0])} style={{padding: "10px 20px", borderTop: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, cursor: "pointer"}}>
<div style={{flex: 1, minWidth: 0}}>
<div style={{fontSize: 13, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}}>{g.name}</div>
<div style={{fontSize: 11, color: "var(--ink-3)"}}>{H.brandName(data, g.brandId)} · {g.type}</div>
</div>
<Pill tone="amber" style={{fontSize: 10}}>{g.totalCount} left</Pill>
</div>
))}
</div>
</Card>
</div>
</div>
);
};
// ─── INVENTORY ─────────────────────────────────────────────────────
const Inventory = ({data, onSelectProduct, onNav}) => {
const [filter, setFilter] = React.useState("active"); // active | consumed | gone | all
const [typeFilter, setTypeFilter] = React.useState("all");
const [sortBy, setSortBy] = React.useState("recent");
const [search, setSearch] = React.useState("");
let products = data.products;
if (filter === "active") products = products.filter(p => p.status === "active");
else if (filter === "consumed") products = products.filter(p => p.status === "consumed");
else if (filter === "gone") products = products.filter(p => p.status === "gone");
if (typeFilter !== "all") products = products.filter(p => p.type === typeFilter);
if (search) {
const q = search.toLowerCase();
products = products.filter(p => {
const brand = H.brandName(data, p.brandId).toLowerCase();
const shop = H.shopName(data, p.shopId).toLowerCase();
return p.name.toLowerCase().includes(q) ||
brand.includes(q) ||
shop.includes(q) ||
p.sku.toLowerCase().includes(q);
});
}
products = [...products].sort((a, b) => {
if (sortBy === "recent") return new Date(b.purchaseDate) - new Date(a.purchaseDate);
if (sortBy === "name") return a.name.localeCompare(b.name);
if (sortBy === "thc") return b.thc - a.thc;
if (sortBy === "remaining") return H.estimatedRemaining(b, TODAY_STR) - H.estimatedRemaining(a, TODAY_STR);
if (sortBy === "price") return b.price - a.price;
if (sortBy === "audit") return H.daysSinceCheck(b, TODAY_STR) - H.daysSinceCheck(a, TODAY_STR);
return 0;
});
return (
<div style={{padding: "32px 40px 80px", maxWidth: 1400, margin: "0 auto"}}>
<div style={{display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 24}}>
<div>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>{products.length} items</div>
<h1 className="serif" style={{fontSize: 44, margin: "6px 0 0", fontWeight: 500, letterSpacing: "-0.02em"}}>Inventory</h1>
</div>
<div style={{display: "flex", gap: 8}}>
<Btn variant="secondary" icon="check" onClick={() => onNav("audit")}>Audit</Btn>
<Btn variant="primary" icon="plus" onClick={() => onNav("add")}>New product</Btn>
</div>
</div>
{/* Toolbar */}
<Card style={{marginBottom: 14, padding: 14}}>
<div style={{display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap"}}>
{/* Tabs */}
<div style={{display: "inline-flex", background: "var(--bg-2)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", padding: 3}}>
{[["active", "Active"], ["consumed", "Consumed"], ["gone", "Gone"], ["all", "All"]].map(([k, l]) => (
<button key={k} onClick={() => setFilter(k)} style={{
padding: "6px 14px",
fontSize: 12,
fontWeight: 500,
borderRadius: 6,
border: "none",
background: filter === k ? "var(--surface)" : "transparent",
color: filter === k ? "var(--ink)" : "var(--ink-3)",
boxShadow: filter === k ? "var(--shadow-sm)" : "none",
cursor: "pointer"
}}>{l}</button>
))}
</div>
{/* Search */}
<div style={{flex: 1, minWidth: 220, display: "flex", alignItems: "center", gap: 8, background: "var(--bg-2)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", padding: "0 10px"}}>
<Icon name="search" size={14} color="var(--ink-3)" />
<input
placeholder="Search by name, brand, shop, SKU…"
value={search}
onChange={e => setSearch(e.target.value)}
style={{border: "none", outline: "none", background: "transparent", padding: "8px 0", fontSize: 13, flex: 1, color: "var(--ink)"}}
/>
</div>
<Select value={typeFilter} onChange={e => setTypeFilter(e.target.value)} style={{...inputStyle, width: "auto", padding: "8px 10px"}}>
<option value="all">All types</option>
{data.types.map(t => <option key={t.id} value={t.id}>{t.id}</option>)}
</Select>
<Select value={sortBy} onChange={e => setSortBy(e.target.value)} style={{...inputStyle, width: "auto", padding: "8px 10px"}}>
<option value="recent">Recent first</option>
<option value="name">Name (AZ)</option>
<option value="thc">THC % (high)</option>
<option value="remaining">Remaining (high)</option>
<option value="price">Price (high)</option>
<option value="audit">Audit overdue first</option>
</Select>
</div>
</Card>
{/* Table */}
<Card padded={false}>
<div style={{display: "grid", gridTemplateColumns: "32px 2fr 1fr 1fr 0.6fr 0.6fr 0.9fr 0.9fr 0.8fr", padding: "12px 20px", borderBottom: "1px solid var(--line)", background: "var(--bg-2)", fontSize: 11, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.08em"}}>
<div></div>
<div>Product</div>
<div>Brand</div>
<div>Shop</div>
<div style={{textAlign: "right"}}>THC %</div>
<div style={{textAlign: "right"}}>Price</div>
<div style={{textAlign: "right"}}>Remaining</div>
<div>Last checked</div>
<div>Bin</div>
</div>
{products.length === 0 && (
<div style={{padding: 60, textAlign: "center", color: "var(--ink-3)"}}>No items match these filters.</div>
)}
{products.map(p => {
const bin = data.bins.find(b => b.id === p.binId);
const pctRemaining = H.pctRemaining(p, TODAY_STR);
const overdue = H.auditOverdue(data, p, TODAY_STR);
const sinceCheck = H.daysSinceCheck(p, TODAY_STR);
const last = H.lastAudit(p);
const isInactive = p.status !== "active";
return (
<div key={p.id} onClick={() => onSelectProduct(p)} className="inv-row" style={{
display: "grid",
gridTemplateColumns: "32px 2fr 1fr 1fr 0.6fr 0.6fr 0.9fr 0.9fr 0.8fr",
padding: "14px 20px",
borderBottom: "1px solid var(--line)",
alignItems: "center",
cursor: "pointer",
opacity: isInactive ? 0.55 : 1,
fontSize: 13
}}>
<div style={{fontFamily: "var(--serif)", fontSize: 18, color: "var(--ink-3)"}}>{TYPE_GLYPHS[p.type]}</div>
<div style={{minWidth: 0}}>
<div style={{fontWeight: 500, color: "var(--ink)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}}>
{p.name}
{p.status === "consumed" && <Pill tone="terra" style={{marginLeft: 6, fontSize: 10}}>Consumed</Pill>}
{p.status === "gone" && <Pill tone="amber" style={{marginLeft: 6, fontSize: 10}}>Gone</Pill>}
{p.status === "active" && overdue && <Pill tone="amber" style={{marginLeft: 6, fontSize: 10}}>Audit due</Pill>}
</div>
<div style={{fontSize: 11, color: "var(--ink-3)", fontFamily: "var(--mono)"}}>{p.sku}{p.assetTag ? ` · ${p.assetTag}` : ""}</div>
</div>
<div style={{color: "var(--ink-2)"}}>{H.brandName(data, p.brandId)}</div>
<div style={{color: "var(--ink-3)", fontSize: 12}}>{H.shopName(data, p.shopId)}</div>
<div style={{textAlign: "right", fontFamily: "var(--mono)", color: "var(--ink-2)"}}>{p.thc.toFixed(1)}</div>
<div style={{textAlign: "right", fontFamily: "var(--mono)"}}>{fmt.money(p.price)}</div>
<div style={{textAlign: "right", display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4}}>
<div style={{fontFamily: "var(--mono)", fontSize: 12}}>{remainingShort(p)}</div>
{p.status === "active" && p.kind === "bulk" && (
<div style={{width: 50, height: 3, background: "var(--bg-3)", borderRadius: 2}}>
<div style={{width: `${pctRemaining*100}%`, height: "100%", background: pctRemaining < 0.25 ? "var(--terracotta)" : pctRemaining < 0.5 ? "var(--amber)" : "var(--sage)", borderRadius: 2}} />
</div>
)}
</div>
<div style={{fontSize: 11, color: overdue ? "var(--terracotta)" : "var(--ink-3)"}}>
{p.status !== "active"
? <span style={{fontStyle: "italic"}}>archived</span>
: last
? <span><span className="mono">{sinceCheck}d</span> ago · {last.mode}</span>
: <span style={{fontStyle: "italic"}}>never</span>}
</div>
<div style={{fontSize: 12, color: "var(--ink-3)"}}>{bin ? bin.name : <span style={{fontStyle: "italic"}}>—</span>}</div>
</div>
);
})}
</Card>
</div>
);
};
// ─── PRODUCT DETAIL ────────────────────────────────────────────────
const ProductDetail = ({product, data, onClose, onConsume, onMarkGone, onAudit, onEdit}) => {
const bin = data.bins.find(b => b.id === product.binId);
const cfg = data.types.find(t => t.id === product.type);
const pctRemaining = H.pctRemaining(product, TODAY_STR);
const est = H.estimatedRemaining(product, TODAY_STR);
const last = H.lastAudit(product);
const overdue = H.auditOverdue(data, product, TODAY_STR);
const sinceCheck = H.daysSinceCheck(product, TODAY_STR);
const isActive = product.status === "active";
return (
<div style={{position: "fixed", inset: 0, background: "oklch(20% 0.02 60 / 0.4)", zIndex: 50, display: "flex", justifyContent: "flex-end"}} onClick={onClose}>
<div onClick={e => e.stopPropagation()} style={{
width: "min(720px, 100vw)",
height: "100%",
background: "var(--bg)",
borderLeft: "1px solid var(--line)",
overflow: "auto",
boxShadow: "var(--shadow-lg)"
}}>
{/* Header */}
<div style={{padding: "20px 32px", borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", justifyContent: "space-between", position: "sticky", top: 0, background: "var(--bg)", zIndex: 1}}>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>Product · {product.sku}</div>
<div style={{display: "flex", gap: 6}}>
{isActive && <Btn variant="ghost" icon="check" onClick={() => onAudit(product)}>Audit</Btn>}
{isActive && <Btn variant="secondary" icon="check" onClick={() => onConsume(product)}>Mark finished</Btn>}
{isActive && <Btn variant="ghost" icon="bin" onClick={() => onMarkGone(product)}>Mark gone</Btn>}
<Btn variant="ghost" icon="close" onClick={onClose} />
</div>
</div>
<div style={{padding: "32px 32px 60px"}}>
{/* Identity */}
<div style={{display: "flex", alignItems: "baseline", gap: 16, marginBottom: 8}}>
<div className="serif" style={{fontSize: 18, color: "var(--ink-3)"}}>{TYPE_GLYPHS[product.type]} {product.type}</div>
{product.status === "consumed" && <Pill tone="terra">Consumed · {fmt.daysAgo(product.consumedDate)}</Pill>}
{product.status === "gone" && <Pill tone="amber">Gone · {fmt.daysAgo(product.goneDate)}</Pill>}
{isActive && overdue && <Pill tone="amber">Audit overdue · {sinceCheck}d</Pill>}
</div>
<h1 className="serif" style={{fontSize: 48, margin: "0 0 4px", fontWeight: 500, letterSpacing: "-0.02em", lineHeight: 1.1}}>{product.name}</h1>
<div style={{fontSize: 16, color: "var(--ink-2)"}}>{H.brandName(data, product.brandId)} · from {H.shopName(data, product.shopId)}</div>
{/* Hero stats */}
<div style={{display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 1, marginTop: 32, background: "var(--line)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", overflow: "hidden"}}>
{[
["Price", fmt.money(product.price)],
[product.kind === "discrete" ? "Quantity" : "Size",
product.kind === "discrete"
? `${product.countOriginal} ${cfg?.unit || "ct"}`
: `${product.weight} ${cfg?.unit || "g"}`
],
["THC", `${product.thc.toFixed(1)}%`],
["CBD", `${product.cbd.toFixed(1)}%`]
].map(([l, v]) => (
<div key={l} style={{padding: "18px 16px", background: "var(--surface)"}}>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>{l}</div>
<div className="serif" style={{fontSize: 26, marginTop: 4, fontWeight: 500}}>{v}</div>
</div>
))}
</div>
{/* Remaining + audit */}
{isActive && (
<div style={{marginTop: 20}}>
<div style={{display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 8}}>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>
{product.kind === "discrete" ? "Units remaining" : "Estimated remaining"}
</div>
<div style={{fontFamily: "var(--mono)", fontSize: 13}}>
{product.kind === "discrete"
? `${product.countLastAudit != null ? product.countLastAudit : product.countOriginal} of ${product.countOriginal}`
: `${est.toFixed(2)} of ${product.weight} ${cfg?.unit || "g"}`}
<span style={{color: "var(--ink-3)", marginLeft: 8}}>{Math.round(pctRemaining*100)}%</span>
</div>
</div>
<div style={{height: 8, background: "var(--bg-3)", borderRadius: 4, overflow: "hidden"}}>
<div style={{width: `${pctRemaining*100}%`, height: "100%", background: pctRemaining < 0.25 ? "var(--terracotta)" : pctRemaining < 0.5 ? "var(--amber)" : "var(--sage)"}} />
</div>
{product.kind === "bulk" && last && (
<div style={{fontSize: 11, color: "var(--ink-3)", marginTop: 6, fontStyle: "italic"}}>
Estimated by linear decay since last {last.mode} on {fmt.dateShort(last.date)} ({last.value}{cfg?.unit}). Re-audit to update.
</div>
)}
</div>
)}
{/* Audit history */}
<div style={{marginTop: 36}}>
<div style={{display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 12}}>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>Audit history</div>
{isActive && <button onClick={() => onAudit(product)} style={{background: "none", border: "none", fontSize: 12, color: "var(--ink-2)", cursor: "pointer", textDecoration: "underline"}}>+ New audit</button>}
</div>
{(!product.audits || product.audits.length === 0) ? (
<div style={{fontSize: 13, color: "var(--ink-3)", fontStyle: "italic", padding: "12px 0"}}>
No audits recorded. Cadence for {product.type}: every {cfg?.cadenceDays || "—"} days.
</div>
) : (
<div style={{display: "flex", flexDirection: "column", gap: 0, border: "1px solid var(--line)", borderRadius: "var(--r-md)", overflow: "hidden"}}>
{[...product.audits].reverse().map((a, i) => (
<div key={i} style={{padding: "12px 16px", borderBottom: i < product.audits.length - 1 ? "1px solid var(--line)" : "none", display: "flex", alignItems: "center", gap: 12, background: "var(--surface)"}}>
<div style={{width: 8, height: 8, borderRadius: "50%", background: a.mode === "weigh" ? "var(--sage)" : a.mode === "estimate" ? "var(--amber)" : "var(--plum)"}} />
<div style={{flex: 1}}>
<div style={{fontSize: 13, fontWeight: 500}}>
{a.mode === "weigh" && "Weighed"}
{a.mode === "estimate" && "Estimated"}
{a.mode === "presence" && (a.confirmedBy === "lost" ? "Marked lost" : "Confirmed presence")}
</div>
<div style={{fontSize: 11, color: "var(--ink-3)"}}>{fmt.date(a.date)} · {fmt.daysAgo(a.date)}</div>
</div>
<div className="mono" style={{fontSize: 13, textAlign: "right"}}>
<div>{a.value} {cfg?.unit}</div>
<div style={{fontSize: 10, color: "var(--ink-3)"}}>was {a.prev} {cfg?.unit}</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Details list */}
<div style={{marginTop: 36}}>
<div className="smallcaps" style={{color: "var(--ink-3)", marginBottom: 12}}>Details</div>
<div style={{display: "grid", gridTemplateColumns: "1fr 1fr", gap: "14px 32px"}}>
{[
["SKU", <span className="mono">{product.sku}</span>],
["Asset tag", product.assetTag ? <span className="mono">{product.assetTag}</span> : <span style={{color: "var(--ink-3)"}}>None</span>],
["Type", `${product.type} · ${product.kind}`],
["Brand", H.brandName(data, product.brandId)],
["Shop", H.shopName(data, product.shopId)],
["Total cannabinoids", `${product.totalCannabinoids.toFixed(1)}%`],
["Purchase date", fmt.date(product.purchaseDate)],
["Bin", bin ? `${bin.name} — ${bin.location}` : <span style={{color: "var(--ink-3)"}}>—</span>],
["Audit cadence", `Every ${cfg?.cadenceDays || "—"} days · ${cfg?.auditMode || "—"}`],
["Cost per gram",
product.kind === "bulk" && product.weight > 0
? fmt.money(product.price / product.weight)
: product.kind === "discrete" && product.unitWeight > 0
? `${fmt.money(product.price / (product.countOriginal * product.unitWeight))} (effective)`
: "—"
],
...(product.status === "consumed" ? [
["Date finished", fmt.date(product.consumedDate)],
["Lasted", `${Math.round((new Date(product.consumedDate) - new Date(product.purchaseDate))/86400000)} days`]
] : []),
...(product.status === "gone" ? [
["Date gone", fmt.date(product.goneDate)],
["After", `${Math.round((new Date(product.goneDate) - new Date(product.purchaseDate))/86400000)} days`]
] : [])
].map(([l, v], i) => (
<div key={i} style={{display: "flex", justifyContent: "space-between", paddingBottom: 12, borderBottom: "1px solid var(--line)"}}>
<span style={{color: "var(--ink-3)", fontSize: 12}}>{l}</span>
<span style={{fontSize: 13, fontWeight: 500, textAlign: "right"}}>{v}</span>
</div>
))}
</div>
</div>
{/* Final notes */}
{(product.status === "consumed" || product.status === "gone") && (
<div style={{marginTop: 36, padding: 24, background: "var(--bg-2)", border: "1px solid var(--line)", borderRadius: "var(--r-md)"}}>
<div style={{display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 12}}>
<div className="smallcaps" style={{color: "var(--ink-3)"}}>{product.status === "gone" ? "Why it's gone" : "Final notes"}</div>
{product.status === "consumed" && (
<div style={{display: "flex", gap: 2}}>
{[1,2,3,4,5].map(n => (
<Icon key={n} name="star" size={14} color={n <= (product.rating || 0) ? "var(--amber)" : "var(--ink-4)"} />
))}
</div>
)}
</div>
<div className="serif" style={{fontSize: 18, lineHeight: 1.5, color: "var(--ink-2)", fontStyle: "italic"}}>
"{product.notes || 'No notes recorded.'}"
</div>
</div>
)}
</div>
</div>
</div>
);
};
Object.assign(window, { Dashboard, Inventory, ProductDetail, remainingDisplay, remainingShort });