Vache prompts. Claude codes.How it works

Polishing Inventory Intelligence: Shared Badges, Single-Pass Queries, and Validation

·4 min read·by Vache Sarkissian
Updated June 3, 2026
·
Reviewed March 30, 2026
linesheetinventoryconvextypescriptrefactor
📚Top of Funnel

Written by Claude (Opus 4.6) Vache prompted, reviewed, and published. The data and benchmarks are real; the prose is AI-generated.

The Polish: Refactoring Inventory Intelligence

Linesheet shipped ABC/XYZ inventory classification in February (ABC analysis for stock coverage, XYZ analysis for demand variability). The feature worked but had three maintainability and performance issues: color logic duplicated across three components, analytics queries running 8+ separate filter passes over arrays, and missing server-side validation on threshold settings.

This post covers how we fixed all three with minimal feature changes:

  1. Extracted badge logic into a shared inventoryBadges.ts module
  2. Single-pass queries using accumulators instead of chained filters
  3. Server-side validation to prevent out-of-range threshold settings

The Badge Problem

Three components display intelligence badges — the dashboard widget (InventoryIntelligence), the styles table (VirtualizedTableBody), and the styles card grid (StylesCardGrid). Each had its own hardcoded color strings for ABC classification, XYZ demand variability, weeks-of-stock thresholds, and reorder flags.

The extracted module at src/lib/inventoryBadges.ts centralizes all of it:

// src/lib/inventoryBadges.ts
export const ABC_COLORS: Record<string, string> = {
  A: "bg-yellow-900/30 text-yellow-400 border-yellow-800",
  B: "bg-blue-900/30 text-blue-400 border-blue-800",
  C: "bg-surface-raised text-text-muted border-border-default",
};
 
export const XYZ_COLORS: Record<string, string> = {
  X: "bg-green-900/30 text-green-400",
  Y: "bg-amber-900/30 text-amber-400",
  Z: "bg-red-900/30 text-red-400",
};
 
export function wosColorClass(wos: number): string {
  if (wos < 4) return "text-red-400";
  if (wos < 8) return "text-amber-400";
  return "text-green-400";
}

The module also exports FLAG_COLORS for stockout/clearance/aging flags, MATRIX_BG for the 9-cell ABC-XYZ matrix background colors, and STRATEGY_LABELS mapping each cell to an action — "AX" maps to "Auto-replenish," "CZ" maps to "Discontinue?"

The card grid component gained intelligence badges in this update. Each style card now shows ABC, XYZ, WOS, and flag badges inline, all consuming the shared constants:

// src/components/styles/StylesCardGrid.tsx
import { ABC_COLORS, XYZ_COLORS, FLAG_COLORS, wosColorClass } from "@/lib/inventoryBadges";

Single-Pass Analytics Query

The getInventoryIntelligence query computes per-style metrics (WOS, ABC rank, XYZ variability, reorder flags, oversupply flags) and then builds summary statistics. Previously, the summary computation called .filter() eight separate times over the intelligence array — once for critical stockouts, once for potential stockouts, once for clearance risk, once for aging, once per ABC category, and so on.

The replacement is a single loop:

// convex/analytics.ts — single-pass summary accumulator
let criticalStockouts = 0, potentialStockouts = 0,
    clearanceRisk = 0, aging = 0, aItemsAtRisk = 0,
    totalInventoryValue = 0;
const abcDist = { A: 0, B: 0, C: 0 };
const xyzDist = { X: 0, Y: 0, Z: 0 };
const matrixCounts: Record<string, number> = {};
 
for (const s of intelligence) {
  if (s.reorderFlag === "Critical Stockout") criticalStockouts++;
  else if (s.reorderFlag === "Potential Stockout") potentialStockouts++;
  if (s.oversupplyFlag === "Clearance Risk") clearanceRisk++;
  else if (s.oversupplyFlag === "Aging") aging++;
  if (s.abcCategory === "A") abcDist.A++;
  else if (s.abcCategory === "B") abcDist.B++;
  else if (s.abcCategory === "C") abcDist.C++;
  // ... XYZ, matrix counts, inventory value
}

One pass instead of eight. For a catalog of 500 styles, that eliminates 3,500 unnecessary iterations. More importantly, the query also added a 180-day time bound on poGroupItems fetches — the previous version collected all received items with no time limit, which meant the query's read set grew without bound as historical data accumulated.

Server-Side Threshold Validation

The analytics settings page lets users configure thresholds that control how styles are classified — the coefficient-of-variation boundary between X and Y demand variability, stockout week thresholds, aging day counts. These were previously validated only on the client.

The analysisSettings.update mutation now enforces bounds:

// convex/analysisSettings.ts
if (args.xThreshold < 0 || args.xThreshold > 2)
  throw new Error("X threshold must be 0–2");
if (args.yThreshold < 0.1 || args.yThreshold > 3)
  throw new Error("Y threshold must be 0.1–3");
if (args.yThreshold <= args.xThreshold)
  throw new Error("Y threshold must be greater than X threshold");
if (args.stockoutWeeks < 1 || args.stockoutWeeks > 52)
  throw new Error("Stockout weeks must be 1–52");

The validation also catches logical errors — clearance weeks must exceed stockout weeks, and aging days must be between 30 and 365.

Query Purity Fix

The InventoryAlerts dashboard component was calling new Date() inside the component body and passing it as a query argument. This is a Convex query purity violation — new Date() returns a different value on every render, which means Convex cannot cache the query result.

The fix wraps it in useMemo:

// src/components/dashboard/InventoryAlerts.tsx
const todayDate = useMemo(() => new Date().toISOString().split("T")[0], []);
const data = useQuery(api.analytics.getInventoryAlerts, { todayDate });

The empty dependency array means todayDate is computed once per mount, giving Convex a stable query key for the lifetime of the component.

Results

Three commits, 17 files changed, 275 insertions, 166 deletions. The net line count went down — extracting the shared badge module removed more duplication than it added, and the single-pass accumulator replaced verbose filter chains. All unused @ts-ignore directives were also cleaned up (the TS2589 depth error they suppressed no longer triggers after the query restructuring). The settings page gained htmlFor/id accessibility pairs on all form inputs and proper dark-theme styling on the threshold controls.

Further Reading

Sources

About the Author

Vache Sarkissian

Building research infrastructure and products at the intersection of knowledge systems and machine learning. Creator of Linesheet Pro, vault-search, and the vachsark learning engine.

View Full Bio →
© 2026 Vache Sarkissian·Built with Claude Code
vachsark.com