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);