[ BLOG // 2026-07-17 // 5 MIN ]

Dispatch Board: Ship Orders from the Warehouse in One Click

Why a delivery note per order wasn't enough — and how a single page grouped by delivery routes, one button per route, and an order timeline transformed warehouse operations.

PROJECTS nextjseshopautodilyworkflowcase-study

When you handle dozens of orders a day, one pick-up counter and three delivery routes, deciding “what goes in the van right now” is not trivial. In a B2B auto-parts e-shop it used to go like this: an admin opened an order, checked if the stock had arrived, created a delivery note if it had, opened the next order — rinse and repeat until every order was checked.

Enter the dispatch board. A single page that shows every order ready to ship, grouped by delivery route. Every order is one row, every route is one block, every delivery note is one click.

What used to take minutes

Imagine the warehouse. Parts from suppliers have gone through intake and sit on the shelf. Orders are waiting for those parts to reach customers. But there was no single list that said: “This is ready. Go.”

The admin had to:

  1. Open the order list.
  2. Click into each one — check item status, figure out if everything is in stock.
  3. If yes, manually create a delivery note.
  4. If no, at least make a mental note of what’s missing.
  5. Along the way, remember which order belongs to which route.

Repeat 40 times a day.

Dispatch board: a bird’s-eye view

The board is built on one straightforward query: every in-flight order that has at least one item on the shelf. No magic, no CRON, no cache. Every board load is a fresh SQL query.

┌─ Route: Kolín-east ─────────────────────────┐
│   KO-2405  Autoservis Novák    4/4 ✅  8,200│  [Issue]
│   KO-2403  Pneuservis Král     2/3 ⚠  3,100│  [Issue]
│   KO-2401  Autodíly CZ          3/3 ✅  5,400│  [Issue]
├─ [Issue entire route in one click] ──────────┤
│                                              │
├─ Route: Kutná Hora ──────────────────────────┤
│   KO-2406  Opravna Svoboda     1/1 ✅    700│  [Issue]
│ ...                                          │
└──────────────────────────────────────────────┘

Each row shows:

  • Order number and customer name — links to the order detail.
  • Readiness4/4 ✅ means all four items are ready, 2/3 ⚠ means two out of three.
  • Value — what’s going out right now, in crowns.
  • Issue button — one click creates the delivery note.

Orders are automatically grouped by the delivery route assigned to each customer. Routes are sorted alphabetically; the no-route bucket (personal pickups) sits at the bottom.

One click per order

When an admin hits Issue, this happens in a single transaction:

  1. Verify the order still exists and isn’t cancelled.
  2. Check that every selected receipt lot belongs to the order and has stock available.
  3. Create a delivery note — directly issued, no draft, because the parts are physically on the shelf and the admin wants them out the door.
  4. If this shipment ships the last piece of every item, the order is automatically completed and the customer gets a notification email.
  5. Log everything to the audit trail.

Everything in one serializable transaction. If two admins hit the button at the same millisecond, one gets “quantity exceeds available stock” — never two delivery notes for the same parts.

That matters. In a shop where “I’ll hit dispatch on my way to the coffee machine” is normal, concurrent clicks happen sooner than you expect.

One click per route

The Issue entire route button does exactly the same thing — but for every ready order on the route at once. Then it opens a PDF with all the delivery notes for that route, ready for printing.

This workflow used to take 15 minutes and required five browser tabs. Now it’s one click, one PDF, one trip to the printer.

// Simplified — the real thing is a server action with a transaction
export async function issueRound(routeName: string) {
  const orders = await loadReadyOrdersOnRoute(routeName);
  for (const order of orders) {
    await issueDeliveryNoteForOrder(order.id, order.lots);
    // Each order is its own transaction — one error won't stop the whole load
  }
  // Notify customers whose orders were completed
  notifyCompleted();
}

The order timeline

Alongside the dispatch board came the OrderTimeline — a visual indicator of an order’s lifecycle right on its detail page.

  [1. New] ──── [2. In stock] ──── [3. Delivery note] ──── [4. Invoiced]

Each step is either done (filled, primary color), current (an outlined ring — what’s next), or future (grey, inactive). The admin reads the order’s state and its next action at a glance, no hunting across the page.

The component is purely declarative: it gets an array of {label, done} and figures out which step is next.

What it brought

  • Time to dispatch one order: from ~30 seconds and three clicks to 1 second and one click.
  • Time to dispatch a whole route: from 5+ minutes to 2 clicks (issue + print).
  • No duplicate documents: transactional isolation handles it automatically.
  • Auto-completion: the admin doesn’t have to remember to close an order when the last piece ships — the system does it.
  • Audit trail: every issued delivery note is logged — who, when, which order.

Technical bits worth noting

Readiness computationbuildDispatchBoard() takes orders, subtracts already-shipped quantities from every line, and drops lines that have nothing left. The result is a list of orders that actually make sense to dispatch.

Shared selectdispatchOrderSelect is defined once and used by both the board and issueRound(). If the data shape changes, TypeScript forces both consumers to update at compile time, not at runtime.

Price without a join — each row’s value is availableQuantity × unitPriceGross. No catalogue join, no cache. The data was already in the order query.

The dispatch board is a feature that isn’t complex on its own — but when it’s missing, operations get stuck on details. One overview, one button, one transaction. Suddenly the warehouse runs itself.

// ALL_POSTS
BACK TO BLOG