Require asset ID scan in audit and consume modals
Build and push image / build (push) Successful in 47s

No default item selection — modals open with a scan prompt.
Form fields appear only after scanning an asset ID.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-04 19:34:31 -04:00
parent e50e8ef1fe
commit bc81cc8d18
2 changed files with 201 additions and 186 deletions
+119 -111
View File
@@ -39,7 +39,7 @@ export function AuditFlow({
.filter((i) => i.status === "active")
.sort((a, b) => helpers.daysSinceCheck(b) - helpers.daysSinceCheck(a));
const [itemId, setItemId] = useState(initialItem?.id ?? overdueFirst[0]?.id ?? "");
const [itemId, setItemId] = useState(initialItem?.id ?? "");
const [date, setDate] = useState(TODAY_STR);
const [confirmedBy, setConfirmedBy] = useState<"asset" | "SKU" | "visual">("asset");
@@ -82,17 +82,17 @@ export function AuditFlow({
}
};
if (!item) return null;
const auditMode = cfg?.auditMode ?? "weigh";
const ml = AUDIT_MODE_LABELS[auditMode] ?? AUDIT_MODE_LABELS.weigh!;
const last = helpers.lastAudit(item);
const prevValue =
item.kind === "discrete"
const last = item ? helpers.lastAudit(item) : null;
const prevValue = item
? item.kind === "discrete"
? item.countLastAudit ?? item.countOriginal
: last
? last.value
: item.weight;
: item.weight
: 0;
const delta = Number(value) - prevValue;
@@ -108,7 +108,7 @@ export function AuditFlow({
boxShadow: "var(--shadow-lg)",
}}
>
<ModalHeader title={ml.title} eyebrow="" onClose={onClose} />
<ModalHeader title={item ? ml.title : "Audit"} eyebrow="" onClose={onClose} />
<div style={{ padding: 32 }}>
<ScanField
@@ -118,114 +118,122 @@ export function AuditFlow({
onMatch={handleScan}
/>
<div
style={{
marginTop: 16,
padding: 16,
background: "var(--bg-2)",
border: "1px solid var(--line)",
borderRadius: "var(--r-md)",
}}
>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div>
<div className="serif" style={{ fontSize: 20, fontWeight: 500 }}>
{item.name}
</div>
<div style={{ fontSize: 12, color: "var(--ink-3)" }}>
<span className="mono">{item.assetId}</span> · {item.type} · {item.kind} · cadence every {cfg?.cadenceDays}d
</div>
</div>
<div style={{ textAlign: "right" }}>
<div className="mono" style={{ fontSize: 11, color: "var(--ink-3)" }}>LAST CHECKED</div>
<div className="serif" style={{ fontSize: 18 }}>
{last ? `${helpers.daysSinceCheck(item)}d ago` : "Never"}
</div>
</div>
{!item ? (
<div style={{ marginTop: 24, textAlign: "center", color: "var(--ink-3)", fontSize: 13, fontStyle: "italic", padding: "24px 0" }}>
Scan an asset ID to continue.
</div>
<div style={{ fontSize: 12, color: "var(--ink-2)", marginTop: 10, fontStyle: "italic" }}>
{ml.desc}
</div>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: auditMode === "presence" ? "1fr 1fr 1fr" : "1fr 1fr",
gap: 16,
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>
<Field label="Date">
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
</Field>
{auditMode === "presence" && (
<Field label="Confirmed by">
<Select
value={confirmedBy}
onChange={(e) => setConfirmedBy(e.target.value as typeof confirmedBy)}
>
<option value="asset">Asset id</option>
<option value="SKU">SKU label</option>
<option value="visual">Visual ID</option>
</Select>
</Field>
)}
</div>
<div
style={{
marginTop: 20,
padding: 14,
background: "var(--surface)",
border: "1px solid var(--line)",
borderRadius: "var(--r-md)",
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: 16,
}}
>
<div>
<div className="smallcaps" style={{ color: "var(--ink-3)" }}>Was</div>
<div className="serif" style={{ fontSize: 22 }}>
{prevValue} {cfg?.unit}
</div>
</div>
<div>
<div className="smallcaps" style={{ color: "var(--ink-3)" }}>Now</div>
<div className="serif" style={{ fontSize: 22, color: "var(--sage)" }}>
{value} {cfg?.unit}
</div>
</div>
<div>
<div className="smallcaps" style={{ color: "var(--ink-3)" }}>Δ since last</div>
) : (
<>
<div
className="serif"
style={{
fontSize: 22,
color: delta < 0 ? "var(--terracotta)" : "var(--ink)",
marginTop: 16,
padding: 16,
background: "var(--bg-2)",
border: "1px solid var(--line)",
borderRadius: "var(--r-md)",
}}
>
{delta.toFixed(item.kind === "discrete" ? 0 : 2)} {cfg?.unit}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div>
<div className="serif" style={{ fontSize: 20, fontWeight: 500 }}>
{item.name}
</div>
<div style={{ fontSize: 12, color: "var(--ink-3)" }}>
<span className="mono">{item.assetId}</span> · {item.type} · {item.kind} · cadence every {cfg?.cadenceDays}d
</div>
</div>
<div style={{ textAlign: "right" }}>
<div className="mono" style={{ fontSize: 11, color: "var(--ink-3)" }}>LAST CHECKED</div>
<div className="serif" style={{ fontSize: 18 }}>
{last ? `${helpers.daysSinceCheck(item)}d ago` : "Never"}
</div>
</div>
</div>
<div style={{ fontSize: 12, color: "var(--ink-2)", marginTop: 10, fontStyle: "italic" }}>
{ml.desc}
</div>
</div>
</div>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: auditMode === "presence" ? "1fr 1fr 1fr" : "1fr 1fr",
gap: 16,
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>
<Field label="Date">
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
</Field>
{auditMode === "presence" && (
<Field label="Confirmed by">
<Select
value={confirmedBy}
onChange={(e) => setConfirmedBy(e.target.value as typeof confirmedBy)}
>
<option value="asset">Asset id</option>
<option value="SKU">SKU label</option>
<option value="visual">Visual ID</option>
</Select>
</Field>
)}
</div>
<div
style={{
marginTop: 20,
padding: 14,
background: "var(--surface)",
border: "1px solid var(--line)",
borderRadius: "var(--r-md)",
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: 16,
}}
>
<div>
<div className="smallcaps" style={{ color: "var(--ink-3)" }}>Was</div>
<div className="serif" style={{ fontSize: 22 }}>
{prevValue} {cfg?.unit}
</div>
</div>
<div>
<div className="smallcaps" style={{ color: "var(--ink-3)" }}>Now</div>
<div className="serif" style={{ fontSize: 22, color: "var(--sage)" }}>
{value} {cfg?.unit}
</div>
</div>
<div>
<div className="smallcaps" style={{ color: "var(--ink-3)" }}>Δ since last</div>
<div
className="serif"
style={{
fontSize: 22,
color: delta < 0 ? "var(--terracotta)" : "var(--ink)",
}}
>
{delta.toFixed(item.kind === "discrete" ? 0 : 2)} {cfg?.unit}
</div>
</div>
</div>
</>
)}
{error && (
<div style={{ marginTop: 14, fontSize: 12, color: "var(--terracotta)" }}>{error}</div>
@@ -234,14 +242,14 @@ export function AuditFlow({
<ModalFooter>
<div style={{ fontSize: 12, color: "var(--ink-3)" }}>
Next audit due in {cfg?.cadenceDays}d
{item ? `Next audit due in ${cfg?.cadenceDays}d` : ""}
</div>
<div style={{ display: "flex", gap: 8 }}>
<Btn variant="ghost" onClick={onClose}>Cancel</Btn>
<Btn
variant="primary"
icon="check"
disabled={audit.isPending}
disabled={audit.isPending || !item}
onClick={() => audit.mutate()}
>
{audit.isPending ? "Saving…" : "Save audit"}