diff --git a/web/src/components/ProductDetail.tsx b/web/src/components/ProductDetail.tsx
index a48bf78..4c5ca89 100644
--- a/web/src/components/ProductDetail.tsx
+++ b/web/src/components/ProductDetail.tsx
@@ -38,8 +38,8 @@ export function ProductDetail({
const bin = data.bins.find((b) => b.id === item.binId);
const cfg = TYPES.find((t) => t.id === item.type);
const product = data.products.find((p) => p.id === item.productId);
- const pctRemaining = helpers.pctRemaining(item, getToday(getStoredTimezone()));
- const est = helpers.estimatedRemaining(item, getToday(getStoredTimezone()));
+ const pctRemaining = helpers.pctRemaining(item);
+ const rem = helpers.remaining(item);
const last = helpers.lastAudit(item);
const overdue = helpers.auditOverdue(item, getToday(getStoredTimezone()));
const sinceCheck = helpers.daysSinceCheck(item, getToday(getStoredTimezone()));
@@ -293,12 +293,12 @@ export function ProductDetail({
}}
>
- {item.kind === "discrete" ? "Units remaining" : "Estimated remaining"}
+ {item.kind === "discrete" ? "Units remaining" : "Remaining"}
{item.kind === "discrete"
? `${item.countLastAudit ?? item.countOriginal} of ${item.countOriginal}`
- : `${est.toFixed(2)} of ${item.weight} ${cfg?.unit ?? "g"}`}
+ : `${rem.toFixed(2)} of ${item.weight} ${cfg?.unit ?? "g"}`}
{Math.round(pctRemaining * 100)}%
@@ -327,8 +327,7 @@ export function ProductDetail({
fontStyle: "italic",
}}
>
- Estimated by linear decay since last {last.mode} on {fmt.dateShort(last.date, getStoredTimezone())} ({last.value.toFixed(2)}
- {cfg?.unit}). Re-audit to update.
+ Last {last.mode} on {fmt.dateShort(last.date, getStoredTimezone())}
)}
{item.containerWeight != null && last && (
@@ -340,7 +339,7 @@ export function ProductDetail({
fontStyle: "italic",
}}
>
- Expected container total: {((item.containerWeight - item.weight) + est).toFixed(2)}g
+ Expected container total: {((item.containerWeight - item.weight) + rem).toFixed(2)}g
)}
diff --git a/web/src/components/modals/AuditFlow.tsx b/web/src/components/modals/AuditFlow.tsx
index 4f8e4e6..a8bbd38 100644
--- a/web/src/components/modals/AuditFlow.tsx
+++ b/web/src/components/modals/AuditFlow.tsx
@@ -55,7 +55,8 @@ export function AuditFlow({
if (i.kind === "discrete") {
return String(i.countLastAudit ?? i.countOriginal);
}
- return helpers.estimatedRemaining(i, getToday(getStoredTimezone())).toFixed(2);
+ const last = helpers.lastAudit(i);
+ return (last ? last.value : i.weight).toFixed(2);
};
const [value, setValue] = useState(initialValueFor(item));
const isConcentrate = item?.type === "Concentrate";
diff --git a/web/src/components/modals/CheckinFlow.tsx b/web/src/components/modals/CheckinFlow.tsx
index fd583ec..b003790 100644
--- a/web/src/components/modals/CheckinFlow.tsx
+++ b/web/src/components/modals/CheckinFlow.tsx
@@ -35,7 +35,8 @@ export function CheckinFlow({
const isBulk = item?.kind === "bulk";
const cfg = item ? TYPES.find((t) => t.id === item.type) : undefined;
- const est = item ? helpers.estimatedRemaining(item, getToday(getStoredTimezone())) : 0;
+ const lastAudit = item ? helpers.lastAudit(item) : null;
+ const rem = item ? (lastAudit ? lastAudit.value : item.weight) : 0;
const checkin = useMutation({
mutationFn: () => {
@@ -63,7 +64,8 @@ export function CheckinFlow({
const scanned = allItems.find((i) => i.id === result.item.id);
if (scanned?.prevBinId) setBinId(scanned.prevBinId);
if (scanned?.kind === "bulk") {
- setRemaining(helpers.estimatedRemaining(scanned, getToday(getStoredTimezone())).toFixed(2));
+ const scanLast = helpers.lastAudit(scanned);
+ setRemaining((scanLast ? scanLast.value : scanned.weight).toFixed(2));
}
}
};
@@ -157,7 +159,7 @@ export function CheckinFlow({
{isBulk && (
{
if (p.type === "Tincture" || p.type === "Edible") return 0;
if (p.kind === "bulk") {
- const est = helpers.estimatedRemaining(p, todayStr);
- return Math.max(0, p.weight - est);
+ const rem = helpers.remaining(p);
+ return Math.max(0, p.weight - rem);
}
const cur = p.countLastAudit ?? p.countOriginal;
return Math.max(0, p.countOriginal - cur) * (p.unitWeight || 0);
@@ -152,16 +152,13 @@ export function computeStats(data: Bootstrap): Stats {
const spend90 = last90p.reduce((s, p) => s + p.price, 0);
const inventoryValue = active.reduce(
- (s, p) => s + p.price * helpers.pctRemaining(p, todayStr),
+ (s, p) => s + p.price * helpers.pctRemaining(p),
0,
);
- // Grams currently on hand: bulk uses estimated remaining; discrete uses
- // (units × per-unit weight). Tincture (ml) and edibles (count) are excluded
- // to match the existing `bulkGrams` convention used for $/g and totals.
const inventoryGrams = active.reduce((s, p) => {
if (p.type === "Tincture" || p.type === "Edible") return s;
- if (p.kind === "bulk") return s + helpers.estimatedRemaining(p, todayStr);
+ if (p.kind === "bulk") return s + helpers.remaining(p);
const cur = p.countLastAudit ?? p.countOriginal;
return s + cur * (p.unitWeight || 0);
}, 0);
@@ -198,10 +195,10 @@ export function computeStats(data: Bootstrap): Stats {
const typeBreakdown: Record = {};
active.forEach((p) => {
let g: number;
- if (p.type === "Tincture") g = helpers.estimatedRemaining(p, todayStr) * 0.5;
+ if (p.type === "Tincture") g = helpers.remaining(p) * 0.5;
else if (p.type === "Edible")
g = (p.countLastAudit ?? p.countOriginal) * 0.3;
- else if (p.kind === "bulk") g = helpers.estimatedRemaining(p, todayStr);
+ else if (p.kind === "bulk") g = helpers.remaining(p);
else g = (p.countLastAudit ?? p.countOriginal) * (p.unitWeight || 0);
if (g > 0) typeBreakdown[p.type] = (typeBreakdown[p.type] || 0) + g;
});
@@ -209,7 +206,7 @@ export function computeStats(data: Bootstrap): Stats {
const flowerEquivalent = active
.filter((p) => p.type === "Flower" || p.type === "Pre-roll")
.reduce((s, p) => {
- if (p.kind === "bulk") return s + helpers.estimatedRemaining(p, todayStr);
+ if (p.kind === "bulk") return s + helpers.remaining(p);
return s + (p.countLastAudit ?? p.countOriginal) * (p.unitWeight || 0);
}, 0);
const daysOfSupply = dailyAvg > 0 ? flowerEquivalent / dailyAvg : 0;
@@ -226,7 +223,7 @@ export function computeStats(data: Bootstrap): Stats {
const overdueAudits = active.filter((p) => helpers.auditOverdue(p, todayStr));
const lowStockBulk = active.filter(
- (p) => p.kind === "bulk" && helpers.pctRemaining(p, todayStr) < 0.25,
+ (p) => p.kind === "bulk" && helpers.pctRemaining(p) < 0.25,
);
// Group discrete instances by product so multiple jars of the same
@@ -299,7 +296,7 @@ export function remainingShort(p: Item): string {
const cur = p.countLastAudit ?? p.countOriginal;
return `${cur} ct`;
}
- const est = helpers.estimatedRemaining(p);
- const trimmed = est.toFixed(2).replace(/\.?0+$/, "") || "0";
+ const rem = helpers.remaining(p);
+ const trimmed = rem.toFixed(2).replace(/\.?0+$/, "") || "0";
return `${trimmed} ${cfg?.unit ?? "g"}`;
}
diff --git a/web/src/types.ts b/web/src/types.ts
index ffba994..b61372f 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -206,28 +206,20 @@ export const helpers = {
if (!cfg) return false;
return this.daysSinceCheck(p, today) >= cfg.cadenceDays;
},
- estimatedRemaining(p: Item, today = TODAY_STR): number {
+ remaining(p: Item): number {
if (p.status !== "active" && p.status !== "checked-out") return 0;
if (p.kind === "discrete") {
return p.countLastAudit ?? p.countOriginal;
}
const last = this.lastAudit(p);
- if (!last) return p.weight;
- const daysSinceBase = Math.max(
- 0,
- Math.floor((+new Date(today) - +new Date(last.date)) / 86_400_000),
- );
- const expectedLifespan =
- p.type === "Flower" ? 35 : p.type === "Concentrate" ? 40 : 90;
- const dailyBurn = p.weight / expectedLifespan;
- return Math.max(0, last.value - dailyBurn * daysSinceBase);
+ return last ? last.value : p.weight;
},
- pctRemaining(p: Item, today = TODAY_STR): number {
+ pctRemaining(p: Item): number {
if (p.kind === "discrete") {
const cur = p.countLastAudit ?? p.countOriginal;
return p.countOriginal > 0 ? cur / p.countOriginal : 0;
}
- const est = this.estimatedRemaining(p, today);
- return p.weight > 0 ? est / p.weight : 0;
+ const rem = this.remaining(p);
+ return p.weight > 0 ? rem / p.weight : 0;
},
};
diff --git a/web/src/views/BinsView.tsx b/web/src/views/BinsView.tsx
index 2a9510d..4fdac13 100644
--- a/web/src/views/BinsView.tsx
+++ b/web/src/views/BinsView.tsx
@@ -2,7 +2,6 @@ import { useMemo, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { Bootstrap, Bin, Item } from "../types.js";
import { helpers, enrichItems } from "../types.js";
-import { getToday, getStoredTimezone } from "../tz.js";
import { remainingShort } from "../stats.js";
import { fmt, TYPE_GLYPHS } from "../format.js";
import { api } from "../api.js";
@@ -124,7 +123,7 @@ export function BinsView({
);
const fillPct = slotsUsed / bin.capacity;
const totalValue = binItems.reduce(
- (s, i) => s + i.price * helpers.pctRemaining(i, getToday(getStoredTimezone())),
+ (s, i) => s + i.price * helpers.pctRemaining(i),
0,
);
return (
diff --git a/web/src/views/CustodyView.tsx b/web/src/views/CustodyView.tsx
index 08c989d..cae9a14 100644
--- a/web/src/views/CustodyView.tsx
+++ b/web/src/views/CustodyView.tsx
@@ -1,7 +1,7 @@
import { useMemo } from "react";
import type { Bootstrap, Item } from "../types.js";
import { helpers, enrichItems } from "../types.js";
-import { getToday, getStoredTimezone } from "../tz.js";
+import { getStoredTimezone } from "../tz.js";
import { remainingShort } from "../stats.js";
import { fmt, TYPE_GLYPHS } from "../format.js";
import { Btn, Card, Icon } from "../components/primitives/index.js";
@@ -128,7 +128,7 @@ function CustodyRow({
onMarkGone: () => void;
}) {
const glyph = TYPE_GLYPHS[item.type] ?? "·";
- const pct = helpers.pctRemaining(item, getToday(getStoredTimezone()));
+ const pct = helpers.pctRemaining(item);
return (
= 10 ? 1 : 2)}
unit="g"
- sub="Estimated remaining across active jars"
+ sub="Remaining across active jars"
/>
)}
{lowBulk.slice(0, 3).map((i) => {
- const pct = helpers.pctRemaining(i, getToday(getStoredTimezone()));
+ const pct = helpers.pctRemaining(i);
return (
i.status === "active");
const totalRemaining = active.reduce((s, i) => {
- if (i.kind === "bulk") return s + helpers.estimatedRemaining(i, getToday(getStoredTimezone()));
+ if (i.kind === "bulk") return s + helpers.remaining(i);
return s + (i.countLastAudit ?? i.countOriginal);
}, 0);
const lastBuy = group.items.reduce((max, i) => {
@@ -591,7 +591,7 @@ function ItemRow({
onToggle: (id: string, shiftKey: boolean) => void;
}) {
const bin = data.bins.find((b) => b.id === i.binId);
- const pctRemaining = helpers.pctRemaining(i, getToday(getStoredTimezone()));
+ const pctRemaining = helpers.pctRemaining(i);
const overdue = helpers.auditOverdue(i, getToday(getStoredTimezone()));
const sinceCheck = helpers.daysSinceCheck(i, getToday(getStoredTimezone()));
const last = helpers.lastAudit(i);