Build a complete warehouse management system with receiving, putaway, picking, and shipping workflows
* Preview is for reference only. Actual results may vary depending on the AI model, variable values, and tools used.
You are a senior full-stack engineer specializing in building enterprise-grade Warehouse Management Systems (WMS) for Malaysian SME businesses. Using Next.js 14 App Router + TypeScript + Tailwind CSS + Supabase + shadcn/ui, build a complete WMS that replaces manual Excel tracking or paid WMS software.
Warehouse Details:
- Warehouse Name: {{warehouse_name}}
- Zone Layout: {{zone_layout}}
- Location Format: {{location_format}}
- Warehouse Staff: {{staff_count}} people
═══════════════════════════════
STEP 1: DATABASE SCHEMA (Supabase / PostgreSQL)
═══════════════════════════════
Create the following tables with all columns, types, constraints, and indexes:
1. products
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- sku: text UNIQUE NOT NULL
- name_zh: text NOT NULL (Chinese product name)
- name_en: text
- category: text (e.g. 'electronics', 'food', 'raw_materials')
- unit: text NOT NULL DEFAULT 'pcs' (pcs/kg/carton/pallet)
- weight_kg: numeric(10,3)
- length_cm, width_cm, height_cm: numeric(8,2)
- barcode: text UNIQUE
- reorder_point: integer DEFAULT 0
- max_stock: integer
- storage_zone: text (preferred zone, e.g. 'B')
- requires_lot: boolean DEFAULT false
- shelf_life_days: integer (for food/perishables)
- status: text DEFAULT 'active' CHECK (status IN ('active','discontinued'))
- created_at: timestamptz DEFAULT now()
- Indexes: on sku, barcode, category
2. locations
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- zone: text NOT NULL (e.g. 'A','B','C','D')
- rack: text NOT NULL (e.g. '03')
- shelf: text NOT NULL (e.g. '02')
- bin: text NOT NULL (e.g. '01')
- location_code: text UNIQUE NOT NULL GENERATED ALWAYS AS (zone||'-'||rack||'-'||shelf||'-'||bin) STORED
- zone_type: text CHECK (zone_type IN ('receiving','storage','picking','shipping','quarantine'))
- capacity_kg: numeric(10,2)
- capacity_units: integer
- current_units: integer DEFAULT 0
- is_active: boolean DEFAULT true
- notes: text
- Indexes: on zone, zone_type, location_code
3. inventory
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- product_id: uuid REFERENCES products(id) NOT NULL
- location_id: uuid REFERENCES locations(id) NOT NULL
- quantity: integer NOT NULL DEFAULT 0 CHECK (quantity >= 0)
- lot_number: text
- manufacture_date: date
- expiry_date: date
- received_at: timestamptz DEFAULT now() (for FIFO ordering)
- unit_cost: numeric(12,2) (cost in RM)
- UNIQUE(product_id, location_id, lot_number)
- Indexes: on product_id, location_id, expiry_date, lot_number
4. purchase_orders
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- po_number: text UNIQUE NOT NULL (e.g. 'PO-2024-0001')
- supplier_name: text NOT NULL
- supplier_ssm: text
- expected_date: date
- status: text DEFAULT 'pending' CHECK (status IN ('pending','partial','completed','cancelled'))
- total_value_rm: numeric(14,2)
- notes: text
- created_by: uuid REFERENCES warehouse_staff(id)
- created_at: timestamptz DEFAULT now()
- Indexes: on status, expected_date
5. po_items
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- po_id: uuid REFERENCES purchase_orders(id) ON DELETE CASCADE
- product_id: uuid REFERENCES products(id)
- ordered_qty: integer NOT NULL
- received_qty: integer DEFAULT 0
- unit_price_rm: numeric(12,2)
- Indexes: on po_id, product_id
6. receiving_records
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- grn_number: text UNIQUE NOT NULL (e.g. 'GRN-2024-0001')
- po_id: uuid REFERENCES purchase_orders(id)
- product_id: uuid REFERENCES products(id)
- received_qty: integer NOT NULL
- rejected_qty: integer DEFAULT 0
- reject_reason: text
- lot_number: text
- manufacture_date: date
- expiry_date: date
- staging_location_id: uuid REFERENCES locations(id)
- qc_status: text DEFAULT 'pending' CHECK (qc_status IN ('pending','passed','failed','partial'))
- qc_notes: text
- received_by: uuid REFERENCES warehouse_staff(id)
- received_at: timestamptz DEFAULT now()
- Indexes: on po_id, product_id, received_at
7. putaway_tasks
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- receiving_record_id: uuid REFERENCES receiving_records(id)
- product_id: uuid REFERENCES products(id)
- quantity: integer NOT NULL
- suggested_location_id: uuid REFERENCES locations(id)
- actual_location_id: uuid REFERENCES locations(id)
- status: text DEFAULT 'pending' CHECK (status IN ('pending','in_progress','completed'))
- assigned_to: uuid REFERENCES warehouse_staff(id)
- completed_at: timestamptz
- Indexes: on status, assigned_to
8. sales_orders
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- so_number: text UNIQUE NOT NULL (e.g. 'SO-2024-0001')
- customer_name: text NOT NULL
- customer_ssm: text
- delivery_address: text
- required_date: date
- status: text DEFAULT 'pending' CHECK (status IN ('pending','picking','packed','shipped','cancelled'))
- total_value_rm: numeric(14,2)
- notes: text
- created_by: uuid REFERENCES warehouse_staff(id)
- created_at: timestamptz DEFAULT now()
- Indexes: on status, required_date
9. picking_orders
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- pick_number: text UNIQUE NOT NULL (e.g. 'PICK-2024-0001')
- so_id: uuid REFERENCES sales_orders(id)
- assigned_to: uuid REFERENCES warehouse_staff(id)
- status: text DEFAULT 'pending' CHECK (status IN ('pending','in_progress','completed','cancelled'))
- started_at: timestamptz
- completed_at: timestamptz
- Indexes: on so_id, status, assigned_to
10. pick_items
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- picking_order_id: uuid REFERENCES picking_orders(id) ON DELETE CASCADE
- product_id: uuid REFERENCES products(id)
- location_id: uuid REFERENCES locations(id)
- lot_number: text
- requested_qty: integer NOT NULL
- picked_qty: integer DEFAULT 0
- pick_sequence: integer (optimized path order)
- status: text DEFAULT 'pending' CHECK (status IN ('pending','picked','short'))
- Indexes: on picking_order_id, location_id
11. shipments
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- shipment_number: text UNIQUE NOT NULL (e.g. 'SHP-2024-0001')
- so_id: uuid REFERENCES sales_orders(id)
- courier: text (e.g. 'Pos Laju', 'J&T Express', 'DHL')
- tracking_number: text
- shipped_at: timestamptz
- shipped_by: uuid REFERENCES warehouse_staff(id)
- total_weight_kg: numeric(10,3)
- total_cartons: integer
- Indexes: on so_id, tracking_number
12. stock_movements (immutable audit trail)
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- movement_type: text NOT NULL CHECK (movement_type IN ('receive','putaway','pick','ship','transfer','adjust','stocktake','return'))
- product_id: uuid REFERENCES products(id)
- from_location_id: uuid REFERENCES locations(id)
- to_location_id: uuid REFERENCES locations(id)
- quantity: integer NOT NULL (positive = in, negative = out)
- lot_number: text
- reference_type: text (e.g. 'GRN', 'PICK', 'SO')
- reference_id: uuid
- reference_number: text
- unit_cost_rm: numeric(12,2)
- notes: text
- performed_by: uuid REFERENCES warehouse_staff(id)
- performed_at: timestamptz DEFAULT now()
- Indexes: on product_id, movement_type, performed_at, reference_number
13. stock_takes
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- stocktake_number: text UNIQUE NOT NULL
- zone: text (null = full warehouse count)
- status: text DEFAULT 'draft' CHECK (status IN ('draft','in_progress','completed','cancelled'))
- started_at: timestamptz
- completed_at: timestamptz
- created_by: uuid REFERENCES warehouse_staff(id)
- approved_by: uuid REFERENCES warehouse_staff(id)
- Index: on status
14. stock_take_items
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- stocktake_id: uuid REFERENCES stock_takes(id) ON DELETE CASCADE
- product_id: uuid REFERENCES products(id)
- location_id: uuid REFERENCES locations(id)
- system_qty: integer
- counted_qty: integer
- variance: integer GENERATED ALWAYS AS (counted_qty - system_qty) STORED
- counted_by: uuid REFERENCES warehouse_staff(id)
- counted_at: timestamptz
- Indexes: on stocktake_id, product_id
15. warehouse_staff
- id: uuid PRIMARY KEY DEFAULT gen_random_uuid()
- auth_user_id: uuid REFERENCES auth.users(id) UNIQUE
- name: text NOT NULL
- ic_number: text (MyKad number)
- role: text CHECK (role IN ('admin','supervisor','receiver','picker','packer','driver'))
- is_active: boolean DEFAULT true
- created_at: timestamptz DEFAULT now()
RLS Policies:
- All authenticated staff can SELECT from products, locations, inventory, picking_orders
- Receivers can INSERT into receiving_records and putaway_tasks
- Pickers can UPDATE pick_items
- Supervisors and admins have full access to all tables
- stock_movements is write-only via Server Actions (no direct frontend updates)
- Always wrap auth.uid() in (select auth.uid()) in all RLS policies for performance
═══════════════════════════════
STEP 2: CORE FEATURE MODULES
═══════════════════════════════
MODULE 1: Dashboard (/dashboard)
Display key operational metrics:
- Today's incoming GRNs, outgoing shipments, pending picking orders
- Total SKUs, total units in stock, total inventory value (RM)
- Space utilization rate (occupied / total locations, with progress bar)
- Low stock alerts (products below reorder_point)
- Expiry alerts (items expiring within 30 days)
- Warehouse floor map visualization: colored zone grid showing occupancy rate (green/yellow/red)
- Live stock movement timeline: last 20 stock_movements entries
- This week's inbound vs outbound volume trend chart (Bar Chart using Recharts)
- Quick action buttons: 'Scan Receive', 'New Pick Order', 'Start Stocktake'
MODULE 2: Goods Receiving (/receiving)
GRN (Goods Receiving Note) workflow:
- List view: all pending/partial POs with supplier, expected date, status filters
- New GRN: select PO, line-by-line confirmation, barcode scan to verify SKU
- Quality check step: mark each line as pass / reject / partial, enter reject reason
- Lot entry: if product.requires_lot = true, force entry of lot_number, manufacture_date, expiry_date
- Auto-assign staging location (nearest available slot in receiving zone)
- Generate GRN document, update po_items.received_qty and purchase_orders.status
- Auto-create putaway_tasks for all received lines
- Write stock_movements record (movement_type = 'receive') for every received line
- Discrepancy handling: flag and notify supervisor when received qty differs from ordered qty
MODULE 3: Putaway Management (/putaway)
Smart putaway suggestions:
- Task list: pending putaway items, quantities, current staging location
- System recommends optimal location: based on product.storage_zone preference + available capacity + FIFO adjacency
- Staff can accept suggestion or manually select any available location
- Confirm putaway by scanning target location QR code
- On completion: update inventory table, decrement from staging location, increment at target location
- Write stock_movements (movement_type = 'putaway')
- Support bulk putaway: complete all lines from one GRN in a single workflow
MODULE 4: Inventory Enquiry (/inventory)
Real-time inventory views:
- Toggle between Product View (aggregated by SKU) and Location View (by physical bin)
- Product View: SKU, name, per-location breakdown, total quantity, total value (RM), status badge (Normal / Low / Out)
- Location View: warehouse floor grid; click any bin to see what's stored there
- Lot traceability: search by lot_number to see all locations holding that batch and its dates
- Multi-filter: by zone, category, stock status, days-to-expiry
- Export: download current stock as Excel/CSV including location, lot, and cost data
- Manual adjustment: supervisor-only; requires adjustment reason; auto-writes stock_movements
MODULE 5: Picking Management (/picking)
Pick order generation and execution:
- Auto-generate picking_order from sales_order with single click
- Pick path optimization: sequence pick_items by zone order (A→B→C→D) to minimize travel distance
- FIFO/FEFO auto-lot selection: for lotted products, automatically allocate from oldest received_at (FIFO) or earliest expiry_date (FEFO for food)
- Mobile-optimized pick execution UI: full-screen card showing current pick task with large Location / SKU / Qty display
- Dual-scan confirmation: scan product barcode + location code to confirm pick, prevents errors
- Short-pick handling: if stock insufficient, flag as 'short', auto-split order or escalate to supervisor
- On pick completion: deduct inventory, write stock_movements (movement_type = 'pick')
- Picker efficiency metrics: items/hour, accuracy rate, completion time per staff member
MODULE 6: Packing and Shipping (/shipping)
Pack and dispatch workflow:
- Completed pick orders appear in 'Ready to Pack' queue
- Packing confirmation: verify pick list, enter number of cartons and total weight
- Select courier: Pos Laju / J&T Express / DHL / Own Vehicle
- Enter tracking number (or reserve API integration slot for auto-booking)
- Generate printable Delivery Order (DO) as PDF
- Update sales_orders.status = 'shipped'
- Write stock_movements (movement_type = 'ship')
- Trigger customer notification via WhatsApp/email (reserve Supabase Edge Function slot)
MODULE 7: Stock Take (/stocktake)
Cycle count and full warehouse count:
- Create stocktake task: full warehouse or zone-specific
- Export blank count sheet (system_qty pre-filled, counted_qty blank)
- Mobile count UI: scan location code → scan product barcode → enter counted quantity
- Real-time variance display: highlight discrepancies (red = shortage, green = surplus)
- Supervisor approval workflow: review variance report before committing adjustments
- On approval: system auto-adjusts inventory to match counted quantities
- Write stock_movements (movement_type = 'stocktake') for all adjusted lines
- Variance analysis report: by product, by zone, with RM value impact
MODULE 8: Analytics & Reports (/analytics)
- Inventory value report: grouped by category, showing quantity, average cost, total RM value
- Inventory turnover: for past 30/90/180 days, outbound quantity / average inventory per SKU
- Slow-moving report: products with zero movement for 90+ days (dead stock)
- Throughput charts: daily/weekly inbound vs outbound volume trend
- Space utilization: zone-by-zone occupancy heatmap
- Staff productivity: receiving, picking, packing counts per staff member
- All reports support date range selection and CSV export
═══════════════════════════════
STEP 3: TECHNICAL IMPLEMENTATION
═══════════════════════════════
Project structure:
src/
├── app/
│ ├── layout.tsx (Inter + Noto Sans SC, dark nav #0F172A)
│ ├── page.tsx (redirect to /dashboard)
│ ├── dashboard/page.tsx
│ ├── receiving/
│ │ ├── page.tsx (GRN list)
│ │ └── [id]/page.tsx (receiving workflow)
│ ├── putaway/page.tsx
│ ├── inventory/page.tsx
│ ├── picking/
│ │ ├── page.tsx
│ │ └── [id]/page.tsx (mobile pick execution)
│ ├── shipping/page.tsx
│ ├── stocktake/page.tsx
│ ├── analytics/page.tsx
│ ├── products/page.tsx (product master management)
│ ├── locations/page.tsx (location master management)
│ └── api/
│ ├── scan/route.ts (barcode lookup endpoint)
│ └── stock/route.ts (stock query endpoint)
├── components/
│ ├── warehouse-map.tsx (SVG floor plan visualization)
│ ├── location-grid.tsx (bin grid with occupancy color coding)
│ ├── barcode-scanner.tsx (@zxing/library camera scan)
│ ├── movement-timeline.tsx (stock movement audit feed)
│ ├── stock-alert-banner.tsx (low stock / expiry alerts)
│ └── pick-task-card.tsx (mobile-optimized pick card)
├── lib/
│ ├── supabase/client.ts
│ ├── supabase/server.ts
│ ├── actions/
│ │ ├── receiving.ts (GRN Server Actions)
│ │ ├── putaway.ts (putaway Server Actions)
│ │ ├── picking.ts (pick Server Actions)
│ │ └── inventory.ts (adjustment Server Actions)
│ └── utils/
│ ├── location-suggest.ts (putaway location algorithm)
│ └── pick-path.ts (pick path optimizer)
└── types/index.ts (all TypeScript types)
UI Design Specifications:
- Navigation: dark #0F172A sidebar with icon + label menu, warehouse name at top in white
- Cards: white background, slate-100 surface, rounded-lg, subtle border, generous padding
- Accent color: yellow #FCD34D for primary action buttons, alert badges, key metrics callouts
- Status badge colors: green = normal, yellow = warning, red = critical/stockout, blue = in-progress
- Warehouse floor map: SVG grid with zone color blocks (Zone A = light blue / B = light green / C = light orange / D = light purple)
- Location occupancy colors: empty = white, <25% = green, 25-75% = yellow, >75% = orange, full = red
- Mobile pick UI: minimum 18-24px font size, buttons at least 56px tall for glove-friendly operation
- All RM amounts formatted as: RM 1,234.56 (Malaysian Ringgit, comma thousands separator)
- All dates formatted as: DD/MM/YYYY (Malaysian convention)
Business Rules to Implement:
- FIFO enforcement: when allocating stock for picking, always SELECT FROM inventory WHERE product_id = ? ORDER BY received_at ASC LIMIT 1
- FEFO override: if product has expiry_date set, ORDER BY expiry_date ASC instead
- Capacity check: before putaway, verify locations.current_units + new_qty <= locations.capacity_units; reject if over capacity
- Low stock trigger: after every outbound movement, check if total inventory for SKU falls below reorder_point; if yes, insert alert record
- Non-negative inventory: all outbound Server Actions must verify sufficient stock before proceeding; return error if insufficient
- Immutable audit trail: inventory quantities may only change via stock_movements-writing Server Actions, never via direct UPDATE on inventory table
Generate complete, runnable code for every module including Server Components, Client Components, Server Actions, and Supabase queries. All forms must use react-hook-form + zod validation. Error handling via error.tsx boundary and toast notifications using sonner.