live in productionDistributor Operations SPA· 2025—2026

Stock Management

Distributor operational tool for receiving stock from suppliers, selling to retailers and proprietors, transfers, returns, and printed invoices. Built around how wholesale actually moves. Separate Vite + React 19 SPA and backend repos, both deployed.

ViteReact 19TanStack QueryTanStack TableZustandreact-pdfTailwind v4Better-Auth
Stock Management — distributor operations dashboard

The brief

A distributor was running wholesale inventory in three spreadsheets and a Telegram group. Stock counts disagreed across them weekly. Retailers got the wrong invoice. Receiving from suppliers and selling to shop owners were both manual data entry into the same file. They asked for “a simple inventory app.”

What they actually needed was a tool that mirrored how stock physically moves through wholesale: procurement, transfers, sales to retailers, returns, printing. Without making someone type the same SKU twice.

The problem

The status quo:

  • Two systems of record: physical stock on the shelf and a spreadsheet that's always one delivery behind.
  • Invoicing as an afterthought: printed from a different tool, manually keyed in, often wrong.
  • No transfer concept: when stock moves between locations it just “disappears” and reappears.
  • No audit trail: when a count is wrong, no one can tell when it went wrong.

The approach

I shipped this as two separate repos: a Vite + React 19 SPA and a TypeScript backend, both deployed on Vercel. That split was deliberate. The frontend is the high-iteration surface the operations team uses daily; the backend is the slower-changing source of truth.

The data model treats every stock change as an immutable movement event (receiving, sale, transfer, adjustment). The current count is derived from the sum. Nothing “updates” a stock level directly. That single decision makes the audit trail free and the bugs visible.

MOVEMENT LOG+ 100 receiving · 2026-05-22− 3 sale · 2026-05-23− 10 transfer · 2026-05-24− 1 sale · 2026-05-25+ 1 return · 2026-05-25SUM()DERIVED COUNT87units on hand · SKU-1042no row in any table directly stores "87"
fig. — movements are the truth; current counts are derived

The hard parts

Printing invoices that look right

Wholesale invoices are a layout problem disguised as a data problem. Headers, line items, totals, footer notes, signature block. It has to print to whatever cheap thermal or A4 printer is on the desk. I render them in-browser with react-pdf, preview them in a react-to-print flow, and let the user save or print. Layout lives in code, not in a template the client has to maintain.

Optimistic UI with a real audit trail

The operations staff don't want to wait for a server round-trip every time they record a sale to a retailer. TanStack Query's optimistic mutations let the UI update instantly while the movement event posts in the background. If the server rejects (price mismatch, out of stock), the optimistic state rolls back and a toast explains why. The movement log records both attempts, so an audit shows what happened.

State that survives a refresh

Carts, draft transfers, draft returns. Anything mid-flight lives in Zustand with localStorage persistence. A staff member can start a sale, get pulled away, come back twenty minutes later, and find the cart exactly as they left it. Small detail, but it determines whether software gets used.

Vite + React 19shell
Fast HMR, fast page load on the operations PC.
TanStack Querydata
Server state + optimistic mutations.
TanStack Tableui
Dense, sortable tables for stock + sales lists.
Zustandstate
Persisted local state for in-flight carts and drafts.
react-pdf + react-to-printprint
Layout-driven invoices, no templates to maintain.
Better-Authauth
Role-scoped sessions: cashier vs admin.

What's running today

2repos · frontend + backend
audit history · derived from events
0manual stock reconciliations · placeholder
A4 / 80mminvoice formats supported

Both the frontend and the backend are deployed and in daily use for the distributor's wholesale operations.

What I'd do differently

The two-repo split was right for this team, but it added deploy-coordination friction. Next time I'd reach for a monorepo from day one. Turbo's pipelines catch the “don't ship the frontend if the backend types changed” case more reliably than a careful git habit.