Pre-rolls, edibles, and vaporizers are sold and reasoned about per unit
("$10 each") rather than as a bag total. The Add and Edit forms now ask
for "Price per unit" when the kind is discrete, and the product drawer
displays the per-unit number with the total as a small subline. Bulk
products (flower, concentrate, tincture) still take and show a total.
The stored price column remains the total, so existing data, spend
totals, sort-by-price, and bin value calculations all keep working
unchanged. Conversion (pricePerUnit × countOriginal) happens at the
form boundary on save and the inverse on edit-modal load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -180,7 +180,31 @@ export function ProductDetail({
|
|||||||
>
|
>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
["Price", fmt.money(product.price)],
|
[
|
||||||
|
"Price",
|
||||||
|
product.kind === "discrete" && product.countOriginal > 0 ? (
|
||||||
|
<>
|
||||||
|
{fmt.money(product.price / product.countOriginal)}
|
||||||
|
<span style={{ fontSize: 14, color: "var(--ink-3)", marginLeft: 4 }}>
|
||||||
|
/unit
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
color: "var(--ink-3)",
|
||||||
|
fontWeight: 400,
|
||||||
|
marginTop: 2,
|
||||||
|
fontFamily: "var(--mono)",
|
||||||
|
letterSpacing: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fmt.money(product.price)} total
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
fmt.money(product.price)
|
||||||
|
),
|
||||||
|
],
|
||||||
[
|
[
|
||||||
product.kind === "discrete" ? "Quantity" : "Size",
|
product.kind === "discrete" ? "Quantity" : "Size",
|
||||||
product.kind === "discrete"
|
product.kind === "discrete"
|
||||||
@@ -189,9 +213,9 @@ export function ProductDetail({
|
|||||||
],
|
],
|
||||||
["THC", `${product.thc.toFixed(1)}%`],
|
["THC", `${product.thc.toFixed(1)}%`],
|
||||||
["CBD", `${product.cbd.toFixed(1)}%`],
|
["CBD", `${product.cbd.toFixed(1)}%`],
|
||||||
] as [string, string][]
|
] as [string, React.ReactNode][]
|
||||||
).map(([l, v]) => (
|
).map(([l, v], i) => (
|
||||||
<div key={l} 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>
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export function AddProductFlow({ data, onClose }: { data: Bootstrap; onClose: ()
|
|||||||
|
|
||||||
const cfg = TYPES.find((t) => t.id === form.type);
|
const cfg = TYPES.find((t) => t.id === form.type);
|
||||||
const isDiscrete = cfg?.kind === "discrete";
|
const isDiscrete = cfg?.kind === "discrete";
|
||||||
|
// form.price is total for bulk, per-unit for discrete.
|
||||||
|
const totalPrice = isDiscrete ? form.price * form.countOriginal : form.price;
|
||||||
const cpg = !isDiscrete && form.weight > 0 ? form.price / form.weight : 0;
|
const cpg = !isDiscrete && form.weight > 0 ? form.price / form.weight : 0;
|
||||||
|
|
||||||
// Find an existing strain matching the current name + brand + type.
|
// Find an existing strain matching the current name + brand + type.
|
||||||
@@ -107,6 +109,7 @@ export function AddProductFlow({ data, onClose }: { data: Bootstrap; onClose: ()
|
|||||||
shopId,
|
shopId,
|
||||||
binId,
|
binId,
|
||||||
kind: isDiscrete ? "discrete" : "bulk",
|
kind: isDiscrete ? "discrete" : "bulk",
|
||||||
|
price: totalPrice,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -274,7 +277,7 @@ export function AddProductFlow({ data, onClose }: { data: Bootstrap; onClose: ()
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
<Field label="Price ($)">
|
<Field label={isDiscrete ? "Price per unit ($)" : "Price ($)"}>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
@@ -297,6 +300,15 @@ export function AddProductFlow({ data, onClose }: { data: Bootstrap; onClose: ()
|
|||||||
<span className="mono" style={{ color: "var(--ink-2)" }}>{fmt.money(cpg)}</span>
|
<span className="mono" style={{ color: "var(--ink-2)" }}>{fmt.money(cpg)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isDiscrete && form.price > 0 && form.countOriginal > 0 && (
|
||||||
|
<div style={{ marginTop: 12, fontSize: 12, color: "var(--ink-3)" }}>
|
||||||
|
Total:{" "}
|
||||||
|
<span className="mono" style={{ color: "var(--ink-2)" }}>{fmt.money(totalPrice)}</span>
|
||||||
|
<span style={{ marginLeft: 6 }}>
|
||||||
|
({form.countOriginal} × {fmt.money(form.price)})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="smallcaps" style={{ color: "var(--ink-3)", margin: "28px 0 16px" }}>Cannabinoid profile</div>
|
<div className="smallcaps" style={{ color: "var(--ink-3)", margin: "28px 0 16px" }}>Cannabinoid profile</div>
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 16 }}>
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 16 }}>
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ export function EditProductFlow({
|
|||||||
}) {
|
}) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
|
||||||
|
const isDiscrete = product.kind === "discrete";
|
||||||
|
// form.price is total for bulk, per-unit for discrete. Convert at I/O boundaries.
|
||||||
|
const initialPrice =
|
||||||
|
isDiscrete && product.countOriginal > 0
|
||||||
|
? product.price / product.countOriginal
|
||||||
|
: product.price;
|
||||||
|
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
name: product.name,
|
name: product.name,
|
||||||
brandId: product.brandId ?? NEW_BRAND,
|
brandId: product.brandId ?? NEW_BRAND,
|
||||||
@@ -30,7 +37,7 @@ export function EditProductFlow({
|
|||||||
weight: product.weight,
|
weight: product.weight,
|
||||||
countOriginal: product.countOriginal,
|
countOriginal: product.countOriginal,
|
||||||
unitWeight: product.unitWeight,
|
unitWeight: product.unitWeight,
|
||||||
price: product.price,
|
price: initialPrice,
|
||||||
thc: product.thc,
|
thc: product.thc,
|
||||||
cbd: product.cbd,
|
cbd: product.cbd,
|
||||||
totalCannabinoids: product.totalCannabinoids,
|
totalCannabinoids: product.totalCannabinoids,
|
||||||
@@ -49,7 +56,7 @@ export function EditProductFlow({
|
|||||||
setForm((f) => ({ ...f, [k]: v }));
|
setForm((f) => ({ ...f, [k]: v }));
|
||||||
|
|
||||||
const cfg = TYPES.find((t) => t.id === product.type);
|
const cfg = TYPES.find((t) => t.id === product.type);
|
||||||
const isDiscrete = product.kind === "discrete";
|
const totalPrice = isDiscrete ? form.price * form.countOriginal : form.price;
|
||||||
const cpg = !isDiscrete && form.weight > 0 ? form.price / form.weight : 0;
|
const cpg = !isDiscrete && form.weight > 0 ? form.price / form.weight : 0;
|
||||||
|
|
||||||
const save = useMutation({
|
const save = useMutation({
|
||||||
@@ -86,7 +93,7 @@ export function EditProductFlow({
|
|||||||
weight: isDiscrete ? undefined : form.weight,
|
weight: isDiscrete ? undefined : form.weight,
|
||||||
countOriginal: isDiscrete ? form.countOriginal : undefined,
|
countOriginal: isDiscrete ? form.countOriginal : undefined,
|
||||||
unitWeight: isDiscrete ? form.unitWeight : undefined,
|
unitWeight: isDiscrete ? form.unitWeight : undefined,
|
||||||
price: form.price,
|
price: totalPrice,
|
||||||
thc: form.thc,
|
thc: form.thc,
|
||||||
cbd: form.cbd,
|
cbd: form.cbd,
|
||||||
totalCannabinoids: form.totalCannabinoids,
|
totalCannabinoids: form.totalCannabinoids,
|
||||||
@@ -262,7 +269,7 @@ export function EditProductFlow({
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
<Field label="Price ($)">
|
<Field label={isDiscrete ? "Price per unit ($)" : "Price ($)"}>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
@@ -285,6 +292,15 @@ export function EditProductFlow({
|
|||||||
<span className="mono" style={{ color: "var(--ink-2)" }}>{fmt.money(cpg)}</span>
|
<span className="mono" style={{ color: "var(--ink-2)" }}>{fmt.money(cpg)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isDiscrete && form.price > 0 && form.countOriginal > 0 && (
|
||||||
|
<div style={{ marginTop: 12, fontSize: 12, color: "var(--ink-3)" }}>
|
||||||
|
Total:{" "}
|
||||||
|
<span className="mono" style={{ color: "var(--ink-2)" }}>{fmt.money(totalPrice)}</span>
|
||||||
|
<span style={{ marginLeft: 6 }}>
|
||||||
|
({form.countOriginal} × {fmt.money(form.price)})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="smallcaps" style={{ color: "var(--ink-3)", margin: "28px 0 16px" }}>Cannabinoid profile</div>
|
<div className="smallcaps" style={{ color: "var(--ink-3)", margin: "28px 0 16px" }}>Cannabinoid profile</div>
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 16 }}>
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 16 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user