Tailor edible ingestion flow: use mg units and hide cannabinoid % fields
Build and push image / build (push) Successful in 59s

Edibles are dosed in milligrams, not grams, and percentage-based cannabinoid
profiles don't apply. Adds weightUnit and showCannabinoidPct to TypeConfig so
the add/edit/detail views adapt per product type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 21:12:28 -04:00
parent a3559062db
commit 9aea9535e6
4 changed files with 124 additions and 105 deletions
+24 -15
View File
@@ -61,7 +61,9 @@ export function ProductDetail({
["Strain", item.name], ["Strain", item.name],
["Brand", helpers.brandName(data, item.brandId)], ["Brand", helpers.brandName(data, item.brandId)],
["Shop", helpers.shopName(data, item.shopId)], ["Shop", helpers.shopName(data, item.shopId)],
["Total cannabinoids", `${item.totalCannabinoids.toFixed(1)}%`], ...(cfg?.showCannabinoidPct !== false
? [["Total cannabinoids", `${item.totalCannabinoids.toFixed(1)}%`] as [string, React.ReactNode]]
: []),
["Purchase date", fmt.date(item.purchaseDate)], ["Purchase date", fmt.date(item.purchaseDate)],
["Bin", isCheckedOut ? "In your custody" : bin ? bin.name : <span style={{ color: "var(--ink-3)" }}></span>], ["Bin", isCheckedOut ? "In your custody" : bin ? bin.name : <span style={{ color: "var(--ink-3)" }}></span>],
["Audit cadence", `Every ${cfg?.cadenceDays ?? "—"} days · ${cfg?.auditMode ?? "—"}`], ["Audit cadence", `Every ${cfg?.cadenceDays ?? "—"} days · ${cfg?.auditMode ?? "—"}`],
@@ -203,10 +205,27 @@ export function ProductDetail({
</div> </div>
)} )}
{(() => {
const statCards: [string, React.ReactNode][] = [
["Price", fmt.money(item.price)],
[
item.kind === "discrete" ? "Unit weight" : "Size",
item.kind === "discrete"
? `${item.unitWeight} ${cfg?.weightUnit ?? "g"}`
: `${item.weight} ${cfg?.unit ?? "g"}`,
],
...(cfg?.showCannabinoidPct !== false
? [
["THC", `${item.thc.toFixed(1)}%`] as [string, React.ReactNode],
["CBD", `${item.cbd.toFixed(1)}%`] as [string, React.ReactNode],
]
: []),
];
return (
<div <div
style={{ style={{
display: "grid", display: "grid",
gridTemplateColumns: "repeat(4, 1fr)", gridTemplateColumns: `repeat(${statCards.length}, 1fr)`,
gap: 1, gap: 1,
marginTop: 32, marginTop: 32,
background: "var(--line)", background: "var(--line)",
@@ -215,25 +234,15 @@ export function ProductDetail({
overflow: "hidden", overflow: "hidden",
}} }}
> >
{( {statCards.map(([l, v], i) => (
[
["Price", fmt.money(item.price)],
[
item.kind === "discrete" ? "Unit weight" : "Size",
item.kind === "discrete"
? `${item.unitWeight} g`
: `${item.weight} ${cfg?.unit ?? "g"}`,
],
["THC", `${item.thc.toFixed(1)}%`],
["CBD", `${item.cbd.toFixed(1)}%`],
] as [string, React.ReactNode][]
).map(([l, v], i) => (
<div key={i} style={{ padding: "18px 16px", background: "var(--surface)" }}> <div key={i} style={{ padding: "18px 16px", background: "var(--surface)" }}>
<div className="smallcaps" style={{ color: "var(--ink-3)" }}>{l}</div> <div className="smallcaps" style={{ color: "var(--ink-3)" }}>{l}</div>
<div className="serif" style={{ fontSize: 26, marginTop: 4, fontWeight: 500 }}>{v}</div> <div className="serif" style={{ fontSize: 26, marginTop: 4, fontWeight: 500 }}>{v}</div>
</div> </div>
))} ))}
</div> </div>
);
})()}
{(isActive || isCheckedOut) && ( {(isActive || isCheckedOut) && (
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
@@ -365,9 +365,9 @@ function InstanceDetailsStep({
weight: last?.weight ?? (isDiscrete ? 0 : 3.5), weight: last?.weight ?? (isDiscrete ? 0 : 3.5),
unitWeight: last?.unitWeight ?? (isDiscrete ? 0.7 : 0), unitWeight: last?.unitWeight ?? (isDiscrete ? 0.7 : 0),
price: initialPrice, price: initialPrice,
thc: last?.thc ?? 22, thc: last?.thc ?? (cfg?.showCannabinoidPct !== false ? 22 : 0),
cbd: last?.cbd ?? 0.4, cbd: last?.cbd ?? (cfg?.showCannabinoidPct !== false ? 0.4 : 0),
totalCannabinoids: last?.totalCannabinoids ?? 26, totalCannabinoids: last?.totalCannabinoids ?? (cfg?.showCannabinoidPct !== false ? 26 : 0),
purchaseDate: TODAY_STR, purchaseDate: TODAY_STR,
}); });
const [newShopName, setNewShopName] = useState(""); const [newShopName, setNewShopName] = useState("");
@@ -572,7 +572,7 @@ function InstanceDetailsStep({
}} }}
> >
{isDiscrete ? ( {isDiscrete ? (
<Field label="Unit weight (g)" span={2} hint="Weight of one unit — for grams stats"> <Field label={`Unit weight (${cfg?.weightUnit ?? "g"})`} span={2} hint="Weight of one unit — for grams stats">
<Input <Input
type="number" type="number"
step="0.1" step="0.1"
@@ -616,6 +616,8 @@ function InstanceDetailsStep({
</div> </div>
)} )}
{cfg?.showCannabinoidPct !== false && (
<>
<div <div
className="smallcaps" className="smallcaps"
style={{ color: "var(--ink-3)", margin: "28px 0 16px" }} style={{ color: "var(--ink-3)", margin: "28px 0 16px" }}
@@ -648,6 +650,8 @@ function InstanceDetailsStep({
/> />
</Field> </Field>
</div> </div>
</>
)}
{error && ( {error && (
<div style={{ marginTop: 14, fontSize: 12, color: "var(--terracotta)" }}>{error}</div> <div style={{ marginTop: 14, fontSize: 12, color: "var(--terracotta)" }}>{error}</div>
@@ -191,7 +191,7 @@ export function EditInventoryFlow({
}} }}
> >
{isDiscrete ? ( {isDiscrete ? (
<Field label="Unit weight (g)" span={2} hint="Weight of one unit — for grams stats"> <Field label={`Unit weight (${cfg?.weightUnit ?? "g"})`} span={2} hint="Weight of one unit — for grams stats">
<Input <Input
type="number" type="number"
step="0.1" step="0.1"
@@ -235,6 +235,8 @@ export function EditInventoryFlow({
</div> </div>
)} )}
{cfg?.showCannabinoidPct !== false && (
<>
<div <div
className="smallcaps" className="smallcaps"
style={{ color: "var(--ink-3)", margin: "28px 0 16px" }} style={{ color: "var(--ink-3)", margin: "28px 0 16px" }}
@@ -267,6 +269,8 @@ export function EditInventoryFlow({
/> />
</Field> </Field>
</div> </div>
</>
)}
{item.audits.length > 0 && ( {item.audits.length > 0 && (
<div <div
+8 -6
View File
@@ -98,6 +98,8 @@ export interface TypeConfig {
cadenceDays: number; cadenceDays: number;
unit: string; unit: string;
weighable: boolean; weighable: boolean;
weightUnit: string;
showCannabinoidPct: boolean;
} }
export interface Bootstrap { export interface Bootstrap {
@@ -112,12 +114,12 @@ export interface Bootstrap {
// Type config lives client-side — static, not user data. // Type config lives client-side — static, not user data.
export const TYPES: TypeConfig[] = [ export const TYPES: TypeConfig[] = [
{ id: "Flower", kind: "bulk", auditMode: "weigh", cadenceDays: 14, unit: "g", weighable: true }, { id: "Flower", kind: "bulk", auditMode: "weigh", cadenceDays: 14, unit: "g", weighable: true, weightUnit: "g", showCannabinoidPct: true },
{ id: "Concentrate", kind: "bulk", auditMode: "estimate", cadenceDays: 21, unit: "g", weighable: true }, { id: "Concentrate", kind: "bulk", auditMode: "estimate", cadenceDays: 21, unit: "g", weighable: true, weightUnit: "g", showCannabinoidPct: true },
{ id: "Tincture", kind: "bulk", auditMode: "estimate", cadenceDays: 30, unit: "ml", weighable: false }, { id: "Tincture", kind: "bulk", auditMode: "estimate", cadenceDays: 30, unit: "ml", weighable: false, weightUnit: "ml", showCannabinoidPct: true },
{ id: "Pre-roll", kind: "discrete", auditMode: "presence", cadenceDays: 30, unit: "ct", weighable: false }, { id: "Pre-roll", kind: "discrete", auditMode: "presence", cadenceDays: 30, unit: "ct", weighable: false, weightUnit: "g", showCannabinoidPct: true },
{ id: "Edible", kind: "discrete", auditMode: "presence", cadenceDays: 60, unit: "ct", weighable: false }, { id: "Edible", kind: "discrete", auditMode: "presence", cadenceDays: 60, unit: "ct", weighable: false, weightUnit: "mg", showCannabinoidPct: false },
{ id: "Vaporizer", kind: "discrete", auditMode: "presence", cadenceDays: 30, unit: "ct", weighable: false }, { id: "Vaporizer", kind: "discrete", auditMode: "presence", cadenceDays: 30, unit: "ct", weighable: false, weightUnit: "g", showCannabinoidPct: true },
]; ];
// User-supplied 6-digit asset ids are printed on a roll of physical tags. // User-supplied 6-digit asset ids are printed on a roll of physical tags.