import { useEffect, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { Bootstrap, Item } from "../../types.js"; import { TYPES, helpers, enrichItems } from "../../types.js"; import { getToday, getStoredTimezone } from "../../tz.js"; import { api } from "../../api.js"; import { Btn, Field, Input, Select } from "../primitives/index.js"; import { ScanField, type ScanResult } from "../ScanField.js"; import { ModalBackdrop, ModalHeader, ModalFooter } from "./ModalChrome.js"; import { useToast } from "../Toast.js"; const AUDIT_MODE_LABELS: Record = { weigh: { title: "Reweigh on a scale", desc: "Place the jar (minus tare) and record the new weight.", }, estimate: { title: "Visual estimate", desc: "Eyeball the remaining amount — quick and approximate.", }, presence: { title: "Confirm presence", desc: "Verify the item is still where you left it. Count units if applicable.", }, }; export function AuditFlow({ data, onClose, item: initialItem, }: { data: Bootstrap; onClose: () => void; item: Item | null; }) { const qc = useQueryClient(); const { toast } = useToast(); const allItems = enrichItems(data); const overdueFirst = [...allItems] .filter((i) => i.status === "active") .sort((a, b) => helpers.daysSinceCheck(b) - helpers.daysSinceCheck(a)); const [itemId, setItemId] = useState(initialItem?.id ?? ""); const [date, setDate] = useState(getToday(getStoredTimezone())); const [confirmedBy, setConfirmedBy] = useState<"asset" | "visual">("asset"); const item = allItems.find((i) => i.id === itemId); const cfg = item ? TYPES.find((t) => t.id === item.type) : undefined; const initialValueFor = (i: Item | undefined): string => { if (!i) return "0"; if (i.kind === "discrete") { return String(i.countLastAudit ?? i.countOriginal); } return helpers.estimatedRemaining(i, getToday(getStoredTimezone())).toFixed(2); }; const [value, setValue] = useState(initialValueFor(item)); const isConcentrate = item?.type === "Concentrate"; const [inputMode, setInputMode] = useState<"direct" | "container">( item?.containerWeight != null ? "container" : "direct", ); const [containerTotal, setContainerTotal] = useState(""); const [error, setError] = useState(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: effectiveMode, value: effectiveValue, confirmedBy: cfg?.auditMode === "presence" ? confirmedBy : undefined, }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["bootstrap"] }); toast(`Audit saved — next due in ${cfg?.cadenceDays ?? "?"}d`); onClose(); }, onError: (e: Error) => setError(e.message), }); const handleScan = (result: ScanResult) => { if (result.kind === "item") { setItemId(result.item.id); } }; const auditMode = cfg?.auditMode ?? "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 ? item.kind === "discrete" ? item.countLastAudit ?? item.countOriginal : last ? last.value : item.weight : 0; const delta = effectiveValue - prevValue; return (
{!item ? (
Scan an asset ID to continue.
) : ( <>
{item.name}
{item.assetId} · {item.type} · {item.kind} · cadence every {cfg?.cadenceDays}d
LAST CHECKED
{last ? `${helpers.daysSinceCheck(item)}d ago` : "Never"}
{ml.desc}
{tare != null && !isConcentrate && (
setInputMode("container")} > Weigh container setInputMode("direct")} > Direct entry
)}
{inputMode === "container" && tare != null ? ( setContainerTotal(e.target.value)} /> ) : ( setValue(e.target.value)} /> )} setDate(e.target.value)} /> {auditMode === "presence" && ( )}
{inputMode === "container" && tare != null && (
Tare (empty jar): {tare.toFixed(2)}g {derivedRemaining != null && ( = 0 ? "var(--sage)" : "var(--terracotta)" }}> {" · "}Product remaining: {derivedRemaining.toFixed(2)}g )}
)}
Was
{item.kind === "discrete" ? prevValue : prevValue.toFixed(2)} {cfg?.unit}
Now
{effectiveValue.toFixed(item.kind === "discrete" ? 0 : 2)} {cfg?.unit}
Δ since last
{delta.toFixed(item.kind === "discrete" ? 0 : 2)} {cfg?.unit}
)} {error && (
{error}
)}
{item ? `Next audit due in ${cfg?.cadenceDays}d` : ""}
Cancel audit.mutate()} > {audit.isPending ? "Saving…" : "Save audit"}
); }