Add container weight tracking for weigh-based concentrate audits
Build and push image / build (push) Successful in 1m6s
Build and push image / build (push) Successful in 1m6s
Record the total weight of a jar (product + container) at acquisition so audits can be done by simply re-weighing the sealed jar. The tare is derived (containerWeight − productWeight), and the audit flow offers a "Weigh container" toggle that auto-calculates remaining product from the scale reading. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -55,18 +55,37 @@ export function AuditFlow({
|
||||
return helpers.estimatedRemaining(i, getToday(getStoredTimezone())).toFixed(2);
|
||||
};
|
||||
const [value, setValue] = useState<string>(initialValueFor(item));
|
||||
const [inputMode, setInputMode] = useState<"direct" | "container">(
|
||||
item?.containerWeight != null ? "container" : "direct",
|
||||
);
|
||||
const [containerTotal, setContainerTotal] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValueFor(item));
|
||||
setInputMode(item?.containerWeight != null ? "container" : "direct");
|
||||
setContainerTotal("");
|
||||
}, [itemId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const tare = item ? helpers.tare(item) : null;
|
||||
const derivedRemaining =
|
||||
tare != null && containerTotal !== ""
|
||||
? parseFloat(containerTotal) - tare
|
||||
: null;
|
||||
|
||||
const effectiveValue =
|
||||
inputMode === "container" && derivedRemaining != null
|
||||
? derivedRemaining
|
||||
: Number(value);
|
||||
const effectiveMode =
|
||||
inputMode === "container" ? "weigh" : (cfg?.auditMode ?? "weigh");
|
||||
|
||||
const audit = useMutation({
|
||||
mutationFn: () =>
|
||||
api.auditInventoryItem(itemId, {
|
||||
date,
|
||||
mode: cfg?.auditMode ?? "weigh",
|
||||
value: Number(value),
|
||||
mode: effectiveMode,
|
||||
value: effectiveValue,
|
||||
confirmedBy: cfg?.auditMode === "presence" ? confirmedBy : undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
@@ -84,7 +103,9 @@ export function AuditFlow({
|
||||
};
|
||||
|
||||
const auditMode = cfg?.auditMode ?? "weigh";
|
||||
const ml = AUDIT_MODE_LABELS[auditMode] ?? AUDIT_MODE_LABELS.weigh!;
|
||||
const ml = inputMode === "container"
|
||||
? { title: "Weigh container", desc: "Place the sealed jar on a scale and enter the total weight. Product remaining is calculated from the tare." }
|
||||
: AUDIT_MODE_LABELS[auditMode] ?? AUDIT_MODE_LABELS.weigh!;
|
||||
|
||||
const last = item ? helpers.lastAudit(item) : null;
|
||||
const prevValue = item
|
||||
@@ -95,7 +116,7 @@ export function AuditFlow({
|
||||
: item.weight
|
||||
: 0;
|
||||
|
||||
const delta = Number(value) - prevValue;
|
||||
const delta = effectiveValue - prevValue;
|
||||
|
||||
return (
|
||||
<ModalBackdrop onClose={onClose}>
|
||||
@@ -156,6 +177,23 @@ export function AuditFlow({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tare != null && (
|
||||
<div style={{ display: "flex", gap: 8, marginTop: 16 }}>
|
||||
<Btn
|
||||
variant={inputMode === "container" ? "primary" : "ghost"}
|
||||
onClick={() => setInputMode("container")}
|
||||
>
|
||||
Weigh container
|
||||
</Btn>
|
||||
<Btn
|
||||
variant={inputMode === "direct" ? "primary" : "ghost"}
|
||||
onClick={() => setInputMode("direct")}
|
||||
>
|
||||
Direct entry
|
||||
</Btn>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
@@ -164,22 +202,33 @@ export function AuditFlow({
|
||||
marginTop: 24,
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
label={
|
||||
item.kind === "discrete"
|
||||
? `Count now (${cfg?.unit})`
|
||||
: auditMode === "weigh"
|
||||
? `Weight now (${cfg?.unit})`
|
||||
: `Estimate now (${cfg?.unit})`
|
||||
}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
step={item.kind === "discrete" ? "1" : "0.1"}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
{inputMode === "container" && tare != null ? (
|
||||
<Field label="Container weight now (g)">
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={containerTotal}
|
||||
onChange={(e) => setContainerTotal(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
) : (
|
||||
<Field
|
||||
label={
|
||||
item.kind === "discrete"
|
||||
? `Count now (${cfg?.unit})`
|
||||
: auditMode === "weigh"
|
||||
? `Weight now (${cfg?.unit})`
|
||||
: `Estimate now (${cfg?.unit})`
|
||||
}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
step={item.kind === "discrete" ? "1" : "0.1"}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
<Field label="Date">
|
||||
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
|
||||
</Field>
|
||||
@@ -196,6 +245,17 @@ export function AuditFlow({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{inputMode === "container" && tare != null && (
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: "var(--ink-3)" }}>
|
||||
Tare (empty jar): {tare.toFixed(2)}g
|
||||
{derivedRemaining != null && (
|
||||
<span style={{ color: derivedRemaining >= 0 ? "var(--sage)" : "var(--terracotta)" }}>
|
||||
{" · "}Product remaining: {derivedRemaining.toFixed(2)}g
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 20,
|
||||
@@ -217,7 +277,7 @@ export function AuditFlow({
|
||||
<div>
|
||||
<div className="smallcaps" style={{ color: "var(--ink-3)" }}>Now</div>
|
||||
<div className="serif" style={{ fontSize: 22, color: "var(--sage)" }}>
|
||||
{value} {cfg?.unit}
|
||||
{effectiveValue.toFixed(item.kind === "discrete" ? 0 : 2)} {cfg?.unit}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user