Search docs

Jump to any table example

Discord—

Inventory Allocation Table

A fulfillment-center grid for SKU allocation: edit demand, lead time, and safety stock inline, expand a SKU to allocate its batches across warehouse locations, and watch reorder points, days of cover, and suggested purchase orders recalculate live.

$ npx shadcn add https://www.shad-table.dev/r/inventory-allocation-table.json

Click a demand, lead-time, or safety-stock value to edit it — every derived column recalculates. Expand a SKU to reallocate its batches.

Batches

Nitrile Gloves M (100 ct)

SKU-3310 · 2 batches

Short

60

of 280 on hand

220 / 400 · 180 open

700

0.0d cover

820

82 × 10/case

Cold Brew Concentrate 1L

SKU-1042 · 3 batches

Reorder

104

of 240 on hand

136 / 180 · 44 open

260

1.5d cover

204

17 × 12/case

180 units on open orders · 136 allocated · 44 still open

LotLocationExpiresOn handAllocatedFree
L-2291WH1 · A-03-2Oct 4, 2026Pick first960
L-2310WH1 · A-04-1Oct 19, 2026144104
L-2188WH2 · C-11-4Sep 23, 2026Expired24—

Thermal Labels 4×6 (roll)

SKU-5061 · 1 batch

Reorder

380

of 380 on hand

0 / 0

450

15.2d cover

100

2 × 50/case

Greek Yogurt 500g

SKU-6204 · 3 batches

Reorder

80

of 120 on hand

40 / 64 · 24 open

86

2.5d cover

32

4 × 8/case

Oat Milk Barista 1L

SKU-2007 · 2 batches

Healthy

180

of 300 on hand

120 / 120

102

10.0d cover

—

Protein Bar Chocolate 12-pk

SKU-4120 · 2 batches

Healthy

300

of 390 on hand

90 / 90

250

10.0d cover

—

Shipping Carton 12×12×8

SKU-7118 · 2 batches

Healthy

750

of 1,000 on hand

250 / 250

480

12.5d cover

—
Totals1,8541,156

How it works

1.Formulas are derived on read, never stored

A row only holds raw inputs — batches, open order quantity, demand, lead time, safety stock. Available, reorder point, days of cover, and the suggested PO are all recomputed by deriveMetrics() from those inputs on every render. That's what makes editing safe: change one batch allocation and every dependent number (the SKU's status, its reorder quantity, the footer total) is correct on the next render, because there's no cached value anywhere to forget to update.

src/components/inventory-allocation/formulas.ts
const reorderPoint = row.dailyDemand * row.leadTimeDays + row.safetyStock
const position = available - unallocated
const shortfall = Math.max(0, reorderPoint - position)
const reorderQty = Math.ceil(shortfall / row.casePack) * row.casePack

2.Computed columns sort by the formula, not by what's on screen

Each formula column uses accessorFn to hand TanStack the derived number, so sorting by Reorder point or Suggested PO works with no custom sortingFn. Status sorts by a severity rank (Short → Reorder → Healthy) rather than alphabetically, so the default sort puts the SKUs that need attention first.

src/components/inventory-allocation/columns.tsx
{
id: 'status',
accessorFn: (row) => statusRank[deriveMetrics(row).status],
cell: ({ row }) => <Badge>{deriveMetrics(row.original).status}</Badge>,
}

3.Edits go through table meta, so columns stay static

The Editable Table builds its columns inside useMemo because each cell closes over component state. Here the write-back functions are passed as the table's meta instead, and cells reach them with table.options.meta. The columns array is a plain module-level constant — it never gets rebuilt on each keystroke or save, and it can be read top to bottom like any other example's columns.tsx.

src/components/inventory-allocation/columns.tsx
cell: ({ row, table }) => (
<EditableNumberCell
value={row.original[field]}
onCommit={(n) => getMeta(table).updateSku(row.original.sku, field, n)}
/>
)

4.Expansion survives edits because it's keyed by SKU

Unlike the Master-Detail Table's Set of ids, this uses TanStack's own expanded state with getRowCanExpand — batches are custom markup, not subRows. Two options make it survive editing: getRowId keys rows by SKU so an open panel follows its row when a re-sort moves it, and autoResetExpanded: false stops TanStack from collapsing every row whenever a save produces a new data array.

src/components/inventory-allocation/data-table.tsx
getRowId: (row) => row.sku,
getRowCanExpand: () => true,
autoResetExpanded: false,

5.Validation uses the whole row, not just the cell

A batch's allocation is limited by two things: what that batch has on hand, and what's left of the SKU's open order after the other batches' allocations. The validator closes over the parent row to check both. An invalid value keeps the input open with the message beneath it instead of silently reverting, so the picker can fix the number in place.

src/components/inventory-allocation/batch-detail.tsx
const allocatedElsewhere = allocated - batch.allocated
validate={(n) => {
if (n > batch.onHand) return `Only ${batch.onHand} on hand`
if (allocatedElsewhere + n > row.orderedQty) return `Max ${row.orderedQty - allocatedElsewhere}`
return null
}}

6.FEFO auto-allocation is just another formula

First-expired, first-out picks from the batch that expires soonest and skips expired stock. allocateFefo() takes a row and returns new batches, so the Auto-allocate button is a single updateRow call — and because nothing derived is stored, the status, reorder quantity, and totals all follow automatically.

src/components/inventory-allocation/formulas.ts
let remaining = row.orderedQty
for (const id of batchesByExpiry) {
const take = Math.min(batch.onHand, remaining)
allocations.set(id, take)
remaining -= take
}