// case-study.md

WMS Mobile App
Hardware-integrated Warehouse Management on Flutter

A Flutter mobile app paired with an existing Nuxt admin and Laravel REST API to run an enterprise logistics operator’s warehouse floor — scan-driven receiving, picking, dispatch, validation, count-sheets, and stock movement, with Bluetooth ESC/POS label printing on Epson TM-P80II and offline-first sync over flaky warehouse Wi-Fi.

client Confidential · under NDA
last touched
role Application Developer
period 2024 — Present
status In production
app version 1.4.27 (build 108)
team Application Developer · in-house
Flutter Dart Hive Bluetooth ESC/POS Nuxt Laravel MySQL SAP integration Android Claude Code AI-Assisted Dev
// receipts · tl;dr
build releases
108
feature modules
13
picking iterations
v1 · v2 · v3
offline resilience
queue-replay ✓
// outcome · what changed
BEFORE

Paper-based picking lists + laptop-tethered label printing; pickers shuttled between aisle and desk.

AFTER

Flutter mobile app with Bluetooth ESC/POS printing on Epson TM-P80II — labels print at the bin.

BEFORE

Manual barcode entry slowed scanning to a crawl; mis-keys triggered downstream reconciliation work.

AFTER

Dual-input scan (camera + keyboard-wedge) — pickers use whatever’s fastest for the SKU class.

BEFORE

Scans lost when WiFi dropped mid-aisle; pickers had to redo work or trust paper backups.

AFTER

Offline replay queue — work continues, scans sync on reconnect, no data loss.

What it is

A Flutter mobile app that replaces paper-based and laptop-bound warehouse operations with a single device a forwarder/picker carries on the floor. 13 feature modules — receiving, picking (three variants: base, enhanced, v3), dispatch, validator, count-sheets, stock movement, audit trail — all syncing to a Laravel REST API that fronts ~250 Eloquent models and ~315 migrations.

Talks Bluetooth ESC/POS to Epson TM-P80II thermal printers, supports both camera scanning (mobile_scanner) and hardware keyboard-wedge scanners (Zebra / Honeywell), and pairs with the existing Nuxt admin UI. Built in-house, pair-programmed with Claude Code throughout — 108 build releases shipped to date.

The bottleneck

Before the mobile app, the warehouse workflows lived on a mix of desktop browser sessions, printed picklists, and manual XLSX exports. That created several friction points:

The asks from operations: scan + confirm in <3 seconds, work even with shaky warehouse Wi-Fi, and keep parity with the existing web WMS as the source of truth.

How I broke it down

What I built

Core mobile flows shipped:

The companion web admin (Nuxt + CoreUI) keeps its existing role — dashboards, reports, master data, and customer-specific portals for the largest tenants (under NDA). The mobile app intentionally does not duplicate those screens; it owns the scan-driven floor operations.

Mobile picking screen — phone-frame mockup with SKU + location + qty card, barcode scan input, recent scans log, Bluetooth-paired Epson TM-P80II indicator, and Print/Confirm action buttons.
MOCKMobile picking flow on a paired warehouse handset · dual-input scan (camera + keyboard-wedge), Hive-backed offline queue, one-tap Bluetooth ESC/POS label print to Epson TM-P80II. SKU + lot codes anonymized.

API contract example — every scan posts a confirm event the server validates against current stock and inbound documents:

~/api/routes/wms.php
// POST /api/wms/pick/confirm
Route::post('/pick/confirm', function (Request $req) {
    $validated = $req->validate([
        'order_id'  => 'required|exists:orders,id',
        'sku'       => 'required|string',
        'qty'       => 'required|integer|min:1',
        'location'  => 'required|string',
        'device_id' => 'required|string',
    ]);
    return PickService::confirm($validated);
});

Offline queue (Flutter side) — every action is recorded locally and replayed automatically when connectivity returns:

~/lib/sync/queue.dart
// Hive-backed retry queue, replays in FIFO order
class SyncQueue {
  final Box<QueuedOp> _box;

  Future<void> enqueue(QueuedOp op) async {
    await _box.add(op);
    if (await Connectivity().checkConnectivity() != ConnectivityResult.none) {
      await drain();
    }
  }

  Future<void> drain() async { /* … */ }
}
Receiving flow & ops dashboard screen captures publishing once the rollout is complete

Tech

Results

build releases
108
feature modules
13
picking iterations
v1 · v2 · v3
offline resilience
queue-replay ✓

The app went from concept to production in active rollout, with 108 build releases shipped to operators as flows hardened. Picker workflows were iterated three times (pickingpicking_enhancedpicking_v3) based on real floor feedback. Bluetooth label printing eliminated the trip back to a workstation between picks for high-velocity lanes.

Specific business figures (pick-time, error rate, end-of-day reconciliation effort, onboarding time for new operators) stay with the operations team; happy to share what we’re seeing in detail on request.

What I’d do again — and differently

Worked well:

Would tighten: