Tailor edible ingestion flow: use mg units and hide cannabinoid % fields
Build and push image / build (push) Successful in 59s
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:
@@ -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,37 +205,44 @@ export function ProductDetail({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
{(() => {
|
||||||
style={{
|
const statCards: [string, React.ReactNode][] = [
|
||||||
display: "grid",
|
["Price", fmt.money(item.price)],
|
||||||
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(item.price)],
|
item.kind === "discrete" ? "Unit weight" : "Size",
|
||||||
[
|
item.kind === "discrete"
|
||||||
item.kind === "discrete" ? "Unit weight" : "Size",
|
? `${item.unitWeight} ${cfg?.weightUnit ?? "g"}`
|
||||||
item.kind === "discrete"
|
: `${item.weight} ${cfg?.unit ?? "g"}`,
|
||||||
? `${item.unitWeight} g`
|
],
|
||||||
: `${item.weight} ${cfg?.unit ?? "g"}`,
|
...(cfg?.showCannabinoidPct !== false
|
||||||
],
|
? [
|
||||||
["THC", `${item.thc.toFixed(1)}%`],
|
["THC", `${item.thc.toFixed(1)}%`] as [string, React.ReactNode],
|
||||||
["CBD", `${item.cbd.toFixed(1)}%`],
|
["CBD", `${item.cbd.toFixed(1)}%`] as [string, React.ReactNode],
|
||||||
] as [string, React.ReactNode][]
|
]
|
||||||
).map(([l, v], i) => (
|
: []),
|
||||||
<div key={i} style={{ padding: "18px 16px", background: "var(--surface)" }}>
|
];
|
||||||
<div className="smallcaps" style={{ color: "var(--ink-3)" }}>{l}</div>
|
return (
|
||||||
<div className="serif" style={{ fontSize: 26, marginTop: 4, fontWeight: 500 }}>{v}</div>
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: `repeat(${statCards.length}, 1fr)`,
|
||||||
|
gap: 1,
|
||||||
|
marginTop: 32,
|
||||||
|
background: "var(--line)",
|
||||||
|
border: "1px solid var(--line)",
|
||||||
|
borderRadius: "var(--r-md)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{statCards.map(([l, v], i) => (
|
||||||
|
<div key={i} 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>
|
</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,38 +616,42 @@ function InstanceDetailsStep({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
{cfg?.showCannabinoidPct !== false && (
|
||||||
className="smallcaps"
|
<>
|
||||||
style={{ color: "var(--ink-3)", margin: "28px 0 16px" }}
|
<div
|
||||||
>
|
className="smallcaps"
|
||||||
Cannabinoid profile
|
style={{ color: "var(--ink-3)", margin: "28px 0 16px" }}
|
||||||
</div>
|
>
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 16 }}>
|
Cannabinoid profile
|
||||||
<Field label="THC %">
|
</div>
|
||||||
<Input
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 16 }}>
|
||||||
type="number"
|
<Field label="THC %">
|
||||||
step="0.1"
|
<Input
|
||||||
value={form.thc}
|
type="number"
|
||||||
onChange={(e) => update("thc", +e.target.value)}
|
step="0.1"
|
||||||
/>
|
value={form.thc}
|
||||||
</Field>
|
onChange={(e) => update("thc", +e.target.value)}
|
||||||
<Field label="CBD %">
|
/>
|
||||||
<Input
|
</Field>
|
||||||
type="number"
|
<Field label="CBD %">
|
||||||
step="0.1"
|
<Input
|
||||||
value={form.cbd}
|
type="number"
|
||||||
onChange={(e) => update("cbd", +e.target.value)}
|
step="0.1"
|
||||||
/>
|
value={form.cbd}
|
||||||
</Field>
|
onChange={(e) => update("cbd", +e.target.value)}
|
||||||
<Field label="Total cannabinoids %">
|
/>
|
||||||
<Input
|
</Field>
|
||||||
type="number"
|
<Field label="Total cannabinoids %">
|
||||||
step="0.1"
|
<Input
|
||||||
value={form.totalCannabinoids}
|
type="number"
|
||||||
onChange={(e) => update("totalCannabinoids", +e.target.value)}
|
step="0.1"
|
||||||
/>
|
value={form.totalCannabinoids}
|
||||||
</Field>
|
onChange={(e) => update("totalCannabinoids", +e.target.value)}
|
||||||
</div>
|
/>
|
||||||
|
</Field>
|
||||||
|
</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,38 +235,42 @@ export function EditInventoryFlow({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
{cfg?.showCannabinoidPct !== false && (
|
||||||
className="smallcaps"
|
<>
|
||||||
style={{ color: "var(--ink-3)", margin: "28px 0 16px" }}
|
<div
|
||||||
>
|
className="smallcaps"
|
||||||
Cannabinoid profile
|
style={{ color: "var(--ink-3)", margin: "28px 0 16px" }}
|
||||||
</div>
|
>
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 16 }}>
|
Cannabinoid profile
|
||||||
<Field label="THC %">
|
</div>
|
||||||
<Input
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 16 }}>
|
||||||
type="number"
|
<Field label="THC %">
|
||||||
step="0.1"
|
<Input
|
||||||
value={form.thc}
|
type="number"
|
||||||
onChange={(e) => update("thc", +e.target.value)}
|
step="0.1"
|
||||||
/>
|
value={form.thc}
|
||||||
</Field>
|
onChange={(e) => update("thc", +e.target.value)}
|
||||||
<Field label="CBD %">
|
/>
|
||||||
<Input
|
</Field>
|
||||||
type="number"
|
<Field label="CBD %">
|
||||||
step="0.1"
|
<Input
|
||||||
value={form.cbd}
|
type="number"
|
||||||
onChange={(e) => update("cbd", +e.target.value)}
|
step="0.1"
|
||||||
/>
|
value={form.cbd}
|
||||||
</Field>
|
onChange={(e) => update("cbd", +e.target.value)}
|
||||||
<Field label="Total cannabinoids %">
|
/>
|
||||||
<Input
|
</Field>
|
||||||
type="number"
|
<Field label="Total cannabinoids %">
|
||||||
step="0.1"
|
<Input
|
||||||
value={form.totalCannabinoids}
|
type="number"
|
||||||
onChange={(e) => update("totalCannabinoids", +e.target.value)}
|
step="0.1"
|
||||||
/>
|
value={form.totalCannabinoids}
|
||||||
</Field>
|
onChange={(e) => update("totalCannabinoids", +e.target.value)}
|
||||||
</div>
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{item.audits.length > 0 && (
|
{item.audits.length > 0 && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
+8
-6
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user