Skip to content
mxxhamPublic

About

WMS CKB Surabaya — bin labels, scanning, 3D stock, FEFO allocation, picklists & wave execution on one Supabase ledger

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

198 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JE-IR WMS logo

JE-IR WMS · CKB Warehouse WMS

PT Cipta Krida Bahari · WSM SUB 2 Surabaya — Shell lubricant warehouse

Bin labels · scanning · 3D stock · FEFO allocation · picklists · waves · audits
One application, one database, one stock ledger.


Next.js React TypeScript Tailwind CSS Supabase Three.js Version GitHub

Features Daily routine Setup Phases Design decisions Test status


The Gudang 3D page: the warehouse drawn as rows of racks, each rack stacked with five levels of bins, colour-coded by stock state, with the navigation and the colour legend in view

Gudang 3D — the whole warehouse in one instanced mesh, drawn only while the camera moves. The legend switches the colour mode: ABC class, stock utilisation, or expiry state. Click a box for its bin detail.


Note

The application UI is in Indonesian. Page, menu, and button names are quoted verbatim in backticks so the instructions below match what is on screen. Everything around them is English.

At a glance

Bins SKUs Stock rows Cartons Migrations SQL tests TS test files
2,570 106 1,790 52,078 60 35 38

Stock figures are the seed (24 Sep 2026 WMS file). Aisles CA–CF; aisle CG does not exist.

Features

AreaWhat it does
LabelsRack column strips A–E on A4 (two 95 × 85 mm columns per sheet) or single 80 × 85 mm cells, vector QR + Code 128, black-and-white mode for thermal printers
ScanPhone camera or USB/Bluetooth scanner, always focused, submits on Enter
3D warehouse2,570 bins in one draw call, rendered on demand, colour by ABC, utilisation or expiry
FEFO allocationFrom the database or the day's WMS file; one bin-choice rule; picklists per shipment, forklift and handpick apart; serpentine pick path
WavesIdempotent posting into the movements ledger; pick and its Bin To Bin as one line; the rest of an opened pallet named on both rows (CF40A01 (dipindah NO 6))
CorrectionsIsi dari picklist (the whole wave from the paper, one posting), Perlu ditangani panel with Perbaiki / Perbaiki semua, Posting sesuai lapangan, Ubah baris, Pecah, Batalkan posting (also pick only), Posting Bin To Bin saja, Tambah item / Tambah order; every risky action lists the rows of other waves it would affect
Finding thingsTap a bin for its stock, open rows and movements; Riwayat per row; filter by SKU or bin, only open, only problems; finished waves fold away
Putaway & pickfacesSheet import with per-row conflict decisions; one fixed pick bin per SKU
Inventory controlHolds, reason codes, four-eyes approval, blind cycle counts, receiving, SAP reconciliation, FEFO compliance, Laporan WMS harian (on hand and remain per day)
AuditsBlind picking audit in staging and at the rack, putaway audit, WMS-file audit; a shipment loads only when every line passes

Full allocation rules: docs/ALLOCATOR.md · Inventory control: docs/INVENTORY_CONTROL.md · Data issues: docs/DATA_ISSUES.md · Column mapping: docs/DATA_MAPPING.md

Label sample — 203 dpi print output

Sample A–E rack label strip, 203 dpi

Daily routine

The order matters: an open wave of an earlier day still reserves stock and brings in Bin To Bin moves, and a database plan made around it does not match the floor.

When Do Where
1. Morning, before anything Close every wave of an earlier date: Selesaikan wave if it shipped, Batalkan if not. Tunda only for an order that really comes back. Wave (Alokasi warns and lists them)
2. After inbound / putaway Import the day's WMS file with Ganti seluruh stok Import
3. Plan Run the day from the database or from the WMS file, review, save Alokasi
4. Pick Post what physically happened: Posting per row on the floor, or Isi dari picklist at the desk to copy a whole filled-in picklist and post it at once. Work the Perlu ditangani panel from the top. Wave
5. Count Finish the count tasks opened by short picks the same day Cycle count
6. End of day Download on hand and remain for SAP Inventory → Stok → Laporan WMS harian

Important

Post only what happened. Posting Bin To Bin saja records that the pallet rest is now in the pickface; pressing it to unblock another wave while the pallet stays put makes the system disagree with the floor.


1. Confirm Before Go-Live

The values below are assumptions. Change each one in the single file or menu named, not in many places.

12 assumptions that must be confirmed before go-live
# Assumption Change in
1 Label strip cell order: A (top) → E (bottom); colours A red, B orange, C yellow, D green, E blue config/warehouse.ts → STRIP_LEVEL_ORDER, LEVEL_COLORS
2 Arrow direction: position 01 = left, 02 = right (facing the rack) config/warehouse.ts → POSITION_ARROW
3 1 rack bin = 1 pallet (capacity 1) bins.capacity
4 Rack dimensions for 3D (bay 2.7 m, level 1.6 m, depth 1.2 m, aisle 3.2 m) are placeholders. The rack layout itself is confirmed: back-to-back blocks, racks 01–20 on the left face, 21–40 on the right face (21 directly behind 01), with a walking lane between one block's right face and the next block's left face Pengaturan (Rak per sisi = 20); pick route: lib/allocator/config.ts → baysPerSide
5 ABC classes derived from 91 K_ONE picking rows: provisional Pengaturan → Hitung ulang (after at least 1 month of picking data)
6 Aisle CG is missing from the warehouse mapping status table and was imported as active Settled 5 Oct 2026: CG does not exist (racks stop at CF); its 400 template bins are blocked (0054) and the engine ignores CG 0054_block_aisle_cg.sql, rackLocationPattern in lib/allocator/config.ts
7 Racks CC19, CC20, CE33 are missing from the data (columns or pillars?) check on site
8 "Near expiry" threshold = 90 days config/warehouse.ts → NEAR_EXPIRY_DAYS
9 Shell lubricant shelf life = 48 months from the production date encoded in the batch code (matches 97% of the 24 Sep batches); SKUs with a different shelf life are entered by hand Pengaturan → Aturan inventory; per SKU in Master item
10 Minimum remaining shelf life to ship = 0 days (no rule from Shell or the customer yet) same as above
11 Adjustment above 20 units needs approval from someone else; count tolerance for classes A/B/C = 0 cartons same as above
12 Scanning the carton barcode when posting a pick is not mandatory (the item master has no EAN yet) same as above, once barcodes are filled in on Master item

Warning

Colour vs thermal printers. A 4-inch direct thermal printer only prints black. The per-level colour bands need one of: pre-printed colour labels, a colour printer, or Black-and-white mode in the Label menu (black band, white text). Colour mode is still offered because it follows the sample photos.

Caution

Data issues in the 24 Sep 2026 WMS file. See docs/DATA_ISSUES.md (bin CC01C01 holds #VALUE!, CE08A01 expired in the year 1930, batches CD39C01/C02 whose values Excel read as dates, 129 SKU-bearing bins with quantity 0, and more). Column mapping: docs/DATA_MAPPING.md.


2. Setup

2.1 Supabase

  1. Create a project at supabase.com (Singapore region).
  2. Apply the migrations in filename order. The set is 60 files, supabase/migrations/0001_schema.sql through supabase/migrations/0060_post_wave_sheet_final.sql; use the CLI rather than the SQL Editor so nothing is skipped or run twice:
    supabase link --project-ref <ref>
    supabase db push
    A project that is already running (0001–0003 applied): apply everything from 0004 onwards. Section 3 maps each range of migrations to the feature it delivers. Two worth knowing about: supabase/migrations/0015_realtime.sql turns on Realtime (pages refresh themselves, a Live dot sits in the title) — without it the pages still work but never receive changes; supabase/migrations/0004_allocation.sql changes the stock identity to bin + SKU + batch + expiry, and old data stays valid.
  3. Run supabase/seed.sql (2,570 bins, 106 SKUs, 1,790 stock rows from the WMS file). The file is about 230 KB; if the editor refuses it, use the CLI: psql "<connection string>" -f supabase/seed.sql.
  4. Site account: create one account under Authentication → Users → Add user (name it e.g. Gudang), then in the SQL Editor:
    update public.profiles set role = 'admin', name = 'Gudang' where id = (select id from auth.users where email = '<site account email>');
    Fill in SITE_ACCOUNT_EMAIL and SITE_ACCOUNT_PASSWORD in the hosting environment (Vercel) and in .env.local. While LOGIN_REQUIRED is not true, every visitor shares this account session, so the app opens straight away. On ckb-wms-v2 migration 0071 names that account gudang and 0072 marks it as the shared account; elsewhere it gets a username from its email and the app marks it the first time it signs a device in. Personal accounts (login). Built, not required yet. To switch on, between shifts: (1) supabase db push and deploy with LOGIN_REQUIRED unset — nothing changes; (2) sign in at /login as gudang with the site-account password, create everyone's accounts on Pengguna and hand out usernames and starting passwords; (3) set LOGIN_REQUIRED=true in Vercel and redeploy — devices still on the shared account are sent to /login on their next page; (4) change gudang's password (Reset kata sandi) and remove SITE_ACCOUNT_EMAIL / SITE_ACCOUNT_PASSWORD. Until step 3, personal accounts protect nothing: anyone can be the shared admin, and Pengguna will not change the shared account. Before step 3, check the user list: every admin created meanwhile keeps admin rights.
  5. Authentication → Providers → Email: turn off "Allow new users to sign up".

Regenerating the seed from a new WMS file:

pip install openpyxl
python3 scripts/generate_seed.py path/to/Warehouse_Management_System.xlsx

For routine updates use the Import menu in the app (recorded as movements), not the seed.

2.2 Local SQL tests (without Supabase)

psql -f supabase/tests/00_local_auth_stub.sql   # auth schema + role stub
for f in supabase/migrations/*.sql; do psql -f "$f"; done
psql -f supabase/seed.sql
psql -f supabase/tests/01_rls_and_stock_rules.sql
psql -f supabase/tests/02_allocation_flow.sql            # every row must PASS
psql -f supabase/tests/03_rolling_execution.sql          # every row must PASS (auth stub, not real Supabase)
psql -f supabase/tests/04_putaway_import.sql             # all PASS; rolls back, safe on local Supabase
psql -f supabase/tests/05_pickfaces_counts_corrections.sql  # same
psql -f supabase/tests/06_audits.sql                     # same
psql -f supabase/tests/07_stock_fixes.sql                # same
psql -f supabase/tests/08_cycle_count.sql                # same
psql -f supabase/tests/09_inventory_control.sql          # inventory control (0016–0023), same
psql -f supabase/tests/10_pick_audit.sql                 # picking audit (0024–0026), same
psql -f supabase/tests/11_rack_audit.sql                 # rack audit (0025, 0030, 0031), same
psql -f supabase/tests/12_putaway_rack_audit.sql         # putaway audit (0027, 0028), same
psql -f supabase/tests/13_sheet_pick_audit.sql           # WMS-file audit (0029, 0032), same
psql -f supabase/tests/14_relocate_actual_left.sql       # relocate actual-left (0033), same
psql -f supabase/tests/15_carry_over.sql                 # parked wave carry-over (0036, 0037), same
psql -f supabase/tests/16_pick_order_guard.sql           # pick order guard (0038), same
psql -f supabase/tests/17_pick_found_elsewhere.sql       # pick found elsewhere (0039), same
psql -f supabase/tests/18_putaway_redirect.sql           # putaway redirect (0042), same
psql -f supabase/tests/19_add_relocation.sql             # add relocation (0045), same
psql -f supabase/tests/20_split_task.sql                 # split task (0046), same
psql -f supabase/tests/21_change_relocation.sql          # change relocation (0047), same
psql -f supabase/tests/22_add_wave_items.sql             # add wave items (0048), same
psql -f supabase/tests/23_change_pick_bin.sql            # change pick bin (0049), same
psql -f supabase/tests/24_change_pick_bin_batch.sql      # pick bin follows batch/expiry (0050), same
psql -f supabase/tests/25_add_wave_items_edits.sql       # Tambah item follows the printed picklist (0051), same
psql -f supabase/tests/26_edit_pick_row.sql              # Ubah baris (0052), same
psql -f supabase/tests/27_unpost_pick_keep_move.sql      # Batalkan posting, pick only (0034/0035), same
psql -f supabase/tests/28_post_move_early.sql            # Posting Bin To Bin saja (0053), same
psql -f supabase/tests/29_block_aisle_cg.sql             # aisle CG blocked (0054), same
psql -f supabase/tests/30_wms_day_report.sql             # Laporan WMS harian (0055), same
psql -f supabase/tests/31_post_as_done.sql               # Posting sesuai lapangan (0056), same
psql -f supabase/tests/32_post_wave_sheet.sql            # Isi dari picklist (0057), same
psql -f supabase/tests/33_bin_checks.sql                 # Cek sisa bin (0058), same
psql -f supabase/tests/34_putaway_moved_in.sql           # putaway of a pallet rest already moved in (0059), same
psql -f supabase/tests/38_putaway_fix.sql                # Sheet benar and Riwayat putaway (0066), same
psql -f supabase/tests/39_pick_line.sql                  # paperless picking, Minta supervisor (0067), same
psql -f supabase/tests/40_rack_scan.sql                  # pick confirmed by the rack label (0068), same
psql -f supabase/tests/41_wave_reopen.sql                # Buka lagi and Pulihkan wave (0069), same
psql -f supabase/tests/42_accounts.sql                   # personal accounts, deactivation, sign-in limit (0071), same

Or all of them at once against a temporary local database: scripts/sql-test.sh.

2.3 Application

cp .env.example .env.local   # fill in the URL, anon key, service role key (Project Settings → API)
npm install
npm run dev                  # http://localhost:3000

Phone cameras need HTTPS (or localhost). To test on a phone during development: npx next dev --experimental-https, or deploy to Vercel.

Other scripts: npm test (the whole TypeScript suite), npm run typecheck, npm run lint, npm run build, npm run seed:generate, and npm run allocate:file for the CLI.

2.4 Deploy to Vercel

Import the repo in Vercel, fill in the same five environment variables, deploy. Vercel detects Next.js on its own (the old vercel.json for a static site has been removed).


3. Phase by Phase: Files, Commands, How to Test

Phase 1 — Schema, migrations, RLS, seed (0001–0003)

Files: supabase/migrations/*, supabase/seed.sql, scripts/generate_seed.py, supabase/tests/*.

Test:

select count(*) from bins;                          -- 2570
select count(*), sum(quantity) from inventory;       -- 1790 | 52078
select * from bin_summary where bin_code = 'CA01C01';

Test the stock rules and RLS on local Postgres (not Supabase): psql -f supabase/tests/00_local_auth_stub.sql, then the migrations, then the seed, then supabase/tests/01_rls_and_stock_rules.sql.

Phase 2 — Import

Files: app/(app)/admin/import/*, lib/import-validate.ts, lib/read-sheet.ts, SQL function import_snapshot.

Test: Import → choose the WMS file → sheet WMS and title row 4 are detected automatically → Validasi. Expected result for the 24 Sep file: 2,468 ok, 146 warnings, 1 error (CC01C01 contains #VALUE!). Importing the same file again after the seed gives 0 movements.

Phase 3 — Labels

Files: lib/labels.ts, app/api/labels/route.ts, app/(app)/labels/*.

Test: Label → one rack → CA / 01 → Strip → create the PDF. A4 portrait, two columns (position 01 and 02) side by side, the same cell as Per sel but 95 mm wide (QR, Code 128, colour band with the bin code, info line), no arrows: 5 + 95 + 10 + 95 + 5 = 210 mm across; three 85 mm levels per sheet, so levels D and E of the same two columns continue on the next sheet (LABEL.a4 in config/warehouse.ts). Cut along the grey outline. Per sel mode is an 80 × 85 mm page. Print at 100% scale (Cetak di skala 100%). Sample output: docs/label-samples/.

Phase 4 — Scan and bin detail

Files: app/(app)/scan/*, app/(app)/bin/[code]/*, components/bin/*, components/scan/*.

Test: open /scan on a phone → scan with the camera → point it at a label → the bin page opens. Type XX99 and you get "not a bin format". A pick above the available stock is rejected by the database.

Phase 5 — 3D warehouse

Files: components/warehouse/*, app/(app)/warehouse/*, app/api/warehouse/route.ts.

Test: /warehouse → switch colour mode → click a box → the detail panel opens. Search for CB12 (all bins of rack CB12) or for SKU 550070612.

Phase 6 — Dashboard and reports

Files: app/(app)/dashboard/page.tsx, app/(app)/movements/page.tsx, app/api/movements/export/route.ts, lib/movement-query.ts.

Test: Dashboard shows 2,570 total bins plus stock on hold, expired and near-expiry stock, the ABC breakdown, and the audit accuracy figures. Mutasi → filter type adjustment → export .xlsx.

Phase 7 — Allocation and waves (0004–0007)

Files: lib/allocator/* (the FEFO engine, pure functions with no I/O), app/(app)/allocate/*, app/(app)/waves/*, supabase/migrations/0004_allocation.sql, docs/ALLOCATOR.md.

Daily flow:

WMS file (sheet "Schedule of the day") ────────┐
                                               ├─► FEFO allocation (browser) ─► Save plan ─► waves + pick_tasks + outbound
Database stock (inventory_detail) ─────────────┘
                                                    (supervisor)                 (stock unchanged)
                                                                     │
            inventory ◀── trigger ◀── movements (picking / transfer) ◀── Post task / Complete wave (operator)
  • Alokasi (supervisor/admin): upload the daily WMS file → date and options → Jalankan alokasi → review the picklist, shortages, planned movements, pickface, and double picks → download PDF/Excel → Simpan rencana. The stock source Sheet WMS di file is a simulation and cannot be saved.
  • Planning stock = physical stock − stock already reserved by open tasks + stock coming in (planning_stock). A plan for another date, or another wave, cannot consume the same cartons.
  • Wave (all roles): confirm one task or one whole wave. Database stock changes at that moment, through the movements ledger.
    • Posting → Sesuai rencana: exactly what the plan said.
    • Posting → differs from the plan: enter the quantity actually taken (0 is allowed), the real source bin and batch, and a reason (mandatory). Stock is deducted from the bin that was really used, and the order records what was taken.
    • Selesaikan wave: every remaining task as planned, in a single transaction.
  • Recalculate remaining waves (supervisor): waves that have not been worked on (still Menunggu, with no task posted or cancelled) are recalculated from current stock, without a file. Waves already running, parked, or cancelled are left alone. Running Alokasi again for the same date also replaces only the untouched waves; shipments of running waves are skipped.
  • Audit gate before loading (all roles): every picked line is checked blind by someone other than the picker before the shipment can be loaded — see Phase 11.

Phase 8 — Putaway, pickfaces, cycle count, data quality (0008–0014)

Files: app/(app)/putaway/*, app/(app)/pickfaces/*, app/(app)/counts/*, app/(app)/data-quality/*, app/(app)/adjust/*, migrations 0008–0014, docs/INVENTORY_CONTROL.md.

  • Putaway (supervisor/admin): upload the WMS file → the data putaway sheet is picked automatically → every row is compared with what the bin holds now. New (empty bin) is posted as a putaway movement; Already recorded (the bin already holds the same thing) is skipped, so the same file is safe to upload again. Conflict is shown next to the bin contents in the system and is not posted until you choose: a different quantity → add to it or match it; a bin holding other stock → put it beside. Unknown bins or SKUs, blocked bins, and duplicate rows are only reported. The conflict report downloads as .xlsx.
  • Pickface (supervisor/admin): one fixed pick bin per SKU. Allocation and Recalculate remaining waves use it for replenishment. An SKU without a pickface keeps using the automatic suggestion (the earliest level-A bin on the route), which never takes another SKU's fixed bin. Fill the suggestions for everything not yet fixed, then Save locks in today's choices.
  • Cycle count (all roles): a count task per bin. Created by hand by a supervisor, from the Data quality page, or automatically when posting a putaway with an undecided quantity or contents conflict (one open task per bin). The operator counts the whole bin without seeing the system quantity; a supervisor sees the difference and then either Applies it (recorded as an adjustment HITUNG <bin>) or Closes without changes with a reason. A count that differs from the bin's class tolerance must be recounted by a different person.
  • Kualitas data (supervisor/admin): a direct check of the stock — rack stock without a batch, batches that are really dates (the original serial number is recommended), missing expiry, already expired on the rack, rack bins holding more than one pallet. Batch and expiry are corrected with two adjustments (KOREKSI <bin>, quantity unchanged); refused while that stock is still used by an open wave task. Expired stock and pallet overflows are sent to Cycle count.
  • Adjust stok (supervisor/admin): swap the contents of two bins, correct an expiry date, or add a SKU the item master does not have yet. Every adjustment needs a reason code and a name.
  • Protected stock: an operator cannot move or pick manually (bin page) stock reserved by open tasks. A supervisor can; tasks that no longer fit are marked red (stok kurang) on the Wave page with a recalculate button.
  • Bin scan: the bin page shows the planned tasks that take from or fill that bin.
  • Stock ledger: Mutasi is every stock change (type, SKU, batch, expiry, quantity, from/to bin, who, when, note; rows coming from wave tasks record the deviation). It cannot be edited or deleted.
  • CLI (no browser): npm run allocate:file -- data/file.xlsx --out out --as-of 2026-09-24 [--db] [--pdf].

Test: npm test. SQL: supabase/tests/04–08.

Phase 9 — Realtime (0015)

Enables Supabase Realtime on the tables the pages subscribe to. Without it the app still works, but the Live indicator never lights up and open pages do not refresh on their own.

Phase 10 — Inventory control (0016–0023)

How it works, the rules, and the daily routine: docs/INVENTORY_CONTROL.md. Migration 0016 adds one policy that every control reads (inventory_policy: shelf life, dispatch minimum, near-expiry days, count tolerance per class, adjustment approval threshold, IRA and picking-accuracy targets, recount-on-variance and scan-on-pick flags) plus the item master fields (barcode, shelf life, minimum remaining shelf life per SKU) and Shell batch-code decoding.

  • 0017 Stock holds — a hold says "this stock may not ship" without moving it: waiting for QC or Shell, damaged, under investigation, recalled, expired, customer return. A hold covers part or all of one stock line.
  • 0018 Adjustment control — every movement carries a reason_code from a fixed list, the name typed on the floor, and a four-eyes approval for movements above the threshold.
  • 0019 Blind counts — the counter never sees the system quantity; a count outside the bin's class tolerance goes to RECOUNT and a different person counts it.
  • 0020 Receiving (Penerimaan) — a receipt is opened from the delivery document number and its lines; the checker at the dock records what is physically there; posting into stock must be done by someone other than the checker.
  • 0021 SAP reconciliation — uploads Shell's SAP book stock per SKU (plant I003 / WHS1, batch "UT", split Unrestricted / Blocked) and snapshots both sides, so a difference can be explained per SKU and per batch.
  • 0022 FEFO compliance — every rack pick in the period where a batch was taken while an older expiry of the same SKU was available in that rack at that moment becomes an exception, plus live page updates.
  • 0023 Pick confirmation — the picker's name on the movement and a carton barcode check: a scanned code must resolve to the task's SKU.

Pages: Inventory with the tabs Stok, Expired & FEFO, Bin kosong, Hold & karantina, Akurasi & adjustment, Rekonsiliasi SAP, Persetujuan; Penerimaan; Master item; and Lacak batch for a batch's whole history.

Test: supabase/tests/09_inventory_control.sql, tests/batch-code.test.ts, tests/sap-stock.test.ts, tests/min-shelf-life.test.ts, tests/stock-accuracy.test.ts.

Phase 11 — Picking and putaway audits (0024–0032)

  • 0024 Picking audit — every picked line is checked blind by someone other than the picker before the shipment is loaded. The checker counts the cartons on the pallet, and the shipment cannot be loaded until every line passes.
  • 0025 Rack audit — instead of walking the staging lanes, the checker walks the racks and counts what is left in each picked bin. What should be left is rebuilt from the picks of the day.
  • 0026 fixes the accuracy KPIs: a wave cancelled after some of its lines were audited no longer counts them.
  • 0027 Putaway audit at the rack — per putaway the checker scans or types the SKU, types its batch, and counts it; the database compares all three.
  • 0028 lets operators be the checker on putaway audits; the named checker may not be the person who did the putaway.
  • 0029 and 0030 run the same two audits against the WMS workbook instead of pick_tasks, for the days when the K_ONE picklist is the source of truth.
  • 0031 lets the checker record a wrong item found in the bin, which becomes its own follow-up.
  • 0032 lets a supervisor re-record a count that was saved by mistake.

Pages: Audit picking (per shipment, per rack, per WMS file, plus an accuracy view and a download per line or per bin) and Audit putaway.

Test: supabase/tests/10–13, tests/pick-audit.test.ts, tests/sheet-audit-export.test.ts, tests/putaway-audit-export.test.ts.

Phase 12 — Wave corrections, floor exceptions and daily reporting (0033–0060)

Everything here exists because of something that happened on the floor; each migration header records the real case.

  • 0033 a relocation carries what is really left in the bin, not what the plan assumed, when a broken pallet is picked short.

  • 0034 Batalkan posting undoes a posting and puts the stock back exactly as the posting took it; an order's quantity can be corrected afterwards. Both stay in the ledger.

  • 0035 a pick and its pallet's leftover move post as one line on the wave page, the way the picklist prints them.

  • 0036 and 0037 a parked (Tunda) wave that comes back on a later schedule — possibly under a new shipment number and with extra items — is carried over instead of being re-planned from scratch.

  • 0038 picks are worked in plan order, and a pick that finds less than planned becomes a count instead of silently taking stock from elsewhere.

  • 0039 a pick whose cartons are physically in another bin than the books say can be posted from that bin (Bin lain).

  • 0040 and 0041 SKUs missing from the item master are added from the WMS file, and an item's description, UoM, UPP, and volume can be edited. UPP decides what counts as a full pallet in the next Alokasi; volume is the litres per unit.

  • 0042 a putaway sheet row whose bin holds other stock can be sent to the bin the pallet really went to (Pindah ke bin lain).

  • 0043 Bin kosong lists active rack bins that hold nothing and are not the target of an open task, so a pallet without a planned destination has somewhere to go.

  • 0044 Tambah order allocates a late order straight onto a day without the Schedule of the day sheet and without re-running Alokasi.

  • 0045 Tambah Bin To Bin adds a move for what a pick leaves behind when the plan had none; 0047 Ubah Bin To Bin sends an open move to a different destination.

  • 0046 Pecah splits an open pick in two so each part can be posted from its own bin.

  • 0048 Tambah item allocates more cartons of a SKU, or a new SKU, onto a wave that already exists.

  • 0049 Ubah Bin Pick lets a supervisor re-point an open pick to a different source bin, and the paired Bin To Bin takes the same source while the move's destination and the pick's batch, expiry and quantity are never changed.

  • 0050 the re-point follows batch and expiry: the suggestion lists every bin + batch + expiry row with its free stock, and a target bin that does not hold the resulting identity is refused instead of landing in stok kurang.

  • 0051 Tambah item rows can follow the printed picklist before saving: any source bin (a later expiry is a warning, not a block), any Bin To Bin destination and sisa, also on a line the engine planned none. A changed source recomputes the sisa from that bin. Every changed row needs a name and a reason and is logged planned → saved; a source bin that does not hold the batch + expiry, or a blocked bin, is refused.

  • 0052 Ubah baris replaces Ubah Bin Pick, Tambah Bin To Bin and Ubah Bin To Bin on open rows: one dialog sets the pick's source (any bin, any batch; a later expiry is warned) and its Bin To Bin (change, add or remove) in one transaction with one name, reason and log line. An open row shows only Posting and ⋯; Ubah baris, Pecah and Batal sit behind ⋯, and Batal no longer asks for confirmation (Pulihkan undoes it).

  • 0053 Posting Bin To Bin saja posts one open move without its pick, also on a wave in Tunda, so a later wave waiting on that pickface (Tunggu relokasi) can go on while the delayed wave keeps its pick open. The button sits in the waiting row's notice and in the pick + Bin To Bin row's ⋯ menu. Batalkan posting on such a row can also undo the pick alone (Pick saja) when the rest in the pickface is already used by other waves.

  • 0054 aisle CG does not exist: its 400 template bins from the WMS sheet are blocked, so they leave Bin kosong and are refused as targets; a later import keeps them blocked.

  • 0055 Laporan WMS harian rebuilds, for any date, the WMS sheet's on hand (after inbound and putaway, before picking) and remain (after picking, the next day's on hand) per bin from the movements ledger: Remain = On hand − PICK − b out + b in on every row, an undone pick counted as un-picking. Downloads as Excel with a per-SKU sheet for SAP.

  • 0056 Posting sesuai lapangan takes what the picker really did — bin + batch, cartons taken, where the pallet rest went — and in one transaction corrects the row (edit_pick_row) and posts the pick and its move; any failure posts nothing.

  • 0057 Isi dari picklist (supervisor; the green button on every wave not cancelled) opens in place of the wave's table, as its printed picklist, with every cell editable: bin, batch, expiry, cartons, a second bin (+ bin lain, the row is split), the Bin To Bin and its sisa, or Belum to leave a row open. Rows already posted can be corrected in the same sheet. One Simpan & posting posts everything in one transaction (post_wave_sheet). Nothing is refused except a bin code that does not exist: the preview names it, and a source the system says is short first gets an open relocation into it posted, then the rest booked as a FOUND adjustment (Koreksi picklist) with a count task on the bin. Every such correction is listed under Koreksi picklist on the Wave page for that wave date (picklist_corrections): SKU, bin, batch, how much was added, system versus paper, the row it came from, the bin's stock now and its count status, with Adjust per line and Unduh Excel.

  • 0058 Cek sisa bin: after a phone Posting that leaves at most bin_check_max_qty cartons (default 5, Pengaturan) of the SKU + batch in the bin, the dialog asks how many are left, blind (Kosong (0) / a number / Lewati). Answers are logged in bin_checks; one that differs from the system opens a count task on the bin. Stock is not changed by the answer.

  • 0059 Putaway of a pallet rest already moved in: a data putaway row into a bin that received the same SKU by Bin To Bin in the last two days is a moved_in conflict (shown with that move). It posts only with Palet baru: tetap posting; left alone it becomes a count task, so a pallet rest is never counted twice.

  • 0060 re-applies 0057's final version (post_wave_sheet, picklist_corrections): 0057 reached live as a draft before it was finished, and an applied migration never runs again.

  • 0066 Sheet benar: betulkan isi bin on Putaway: for a conflict where the sheet is right and the system's bin is wrong (8 Oct: CA06C01 held 4 IAGO drums, the system 36 Helix that were really in CA02D02), every line the system holds in the bin gets a decision — Tetap, Jumlah sebenarnya, Tidak ada (hapus) or Pindah ke bin — then the sheet's pallet is put away. Moves post as transfers; adjustments within adjust_approval_qty post as COUNT_VARIANCE, larger ones wait in Adjust stok for a second person. A fix is refused per row (and becomes a count task) when the bin changed since the check, or the stock is reserved for an open wave or held. Posting asks for a name and confirms with one sentence per bin. Riwayat putaway (putaway_log) keeps every posted sheet line for 30 days on the page: what the sheet said, the conflict, the bin as the system had it at that moment, the decision and its result, with links to the request and count task, so a conflict stays visible after it is resolved.

  • 0067 Paperless picking — Mulai picking on every running wave opens the wave line by line on the phone, in picklist order, for every role. Per line Sesuai posts the plan, Berbeda posts what happened: Qty in the item's unit (Carton, Drum, Pail…), the source bin and batch (scan the location label or type), + bin lain, and where the pallet rest went. A move-only Bin To Bin needs its destination on the picking screen, because a picker cannot cancel a move. post_pick_line posts it through the Isi dari picklist engine (post_wave_sheet_rows) with the picker's limits: a name, at most the order line, a Koreksi picklist only up to adjust_approval_qty, and Ubah only on one's own line while the wave runs and the line is neither audited nor loaded. Anything beyond that is recorded in pick_line_requests and listed under Perlu ditangani · Minta supervisor, where Isi dari picklist opens at that line with what the picker posted. The engine also no longer books a pallet rest again as a Koreksi picklist when that rest was already moved with Posting Bin To Bin saja. The top of the screen shows needed vs picked per unit for the wave, each shipment (lengkap / kurang), each order line (a shortage the allocation never planned shown apart from a short pick) and each picker.

  • 0068 Rack scan — the barcodes on the floor are on the racking, so every posting at a bin (Mulai picking, Posting, a pick with its pallet move, Ambil dari bin lain) is confirmed by that bin's location label, scanned or typed, never by an item barcode (items.ean is empty). A wrong label is refused before anything posts: Bin salah: kamu scan X, baris ini Y. Each completed task records scan_method (scan / ketik) and the confirmed code; unpost_task clears them.

  • 0069 Buka lagi (a finished wave) and Pulihkan wave (a cancelled one), supervisor and admin, with a name and a reason. Buka lagi sets the wave and its orders running again and offers to undo the lines Selesaikan wave posted in bulk (pick_tasks.bulk_source), newest first, each stock movement reversed into the exact bin, batch and expiry it left; lines posted one by one stay posted. It refuses when a shipment is already loaded or a bulk line was audited. Pulihkan wave returns the lines cancelled with the wave to the plan and moves no stock; it warns, before restoring, about a bin that no longer covers its line and offers Hitung ulang wave tersisa. Neither changes Remain Qty except by those reversing movements.

  • 0070 fefo_exceptions (the Dashboard's FEFO compliance tile) rewritten to read each bin's movements through its index instead of re-scanning the period for every pick: same rows, 39 s down to 0.1 s on the local copy. Before, the tile timed out on every load.

  • 0071 Personal accounts (login, not required yet: LOGIN_REQUIRED): profiles.username / active, admin-only set_user_role / set_user_active, account_email for the server-side sign-in, deactivation enforced before every request and in every read/insert policy, and a per-username + IP sign-in limit. See §2 for switching it on.

  • 0072 marks the shared site account (app_metadata.shared): with login required its sessions are signed out; with login not required Pengguna leaves it alone.

  • Bandingkan dengan sistem (Import page, after validation): the WMS file against the system's stock now, nothing imported. Per bin and SKU: same, batch/expiry name only, moved to another bin (the SKU total agrees), quantity differs, only in the file, only in the system; filter chips and Unduh selisih. The way to run the system next to the WMS sheet until they agree every morning.

  • Undoing a posted Bin To Bin asks for confirmation that the cartons physically went back; a move that happened (to another bin or quantity) is corrected with Isi dari picklist or Mutasi, not undone.

Page-level tools in the same phase, no migration of their own: the Perlu ditangani panel lists every row of the day that needs a person (stok kurang, waiting on a Bin To Bin, a short pick's count) with its button, and Perbaiki semua proposes fixes for all stok kurang rows at once, never giving two rows the same cartons; tapping a bin opens its stock, open rows and movements; Riwayat shows a row's decisions and stock movements in order; Tunda, Posting Bin To Bin saja, Batalkan posting, Ubah baris and Perbaiki list the rows of other waves they would make stok kurang or hold up (the database's own rules replayed, lib/wave-impact.ts), and Posting Bin To Bin saja asks for the physical move first; a filter bar and folded finished waves keep the page short. Perbaiki on a stok kurang row proposes the smallest fix in one sentence (shrink the Bin To Bin, or the bin the engine's own selectNextBin picks from current free stock) and saves it through Ubah baris; the Bin To Bin column names where an opened pallet's rest goes when another wave moves it; cancelled rows hide behind Tampilkan dibatalkan; Alokasi and Import warn about open waves of earlier dates.

Wave page layout. One address, two layouts: under 768 px a phone layout of its own, wider the desktop table. Every wave shows one sentence saying what to do next and at most one button that does it (Mulai picking, Lihat, Tutup wave, Aktifkan); everything else is in its Lainnya menu (Picklist, Tambah item, Selesaikan wave, Tunda, Buka lagi, Pulihkan wave, and Batalkan wave last, in red). An open line keeps Posting in view and every other line action in its own menu; the day's tools (Tambah order, the PDFs, Hitung ulang wave tersisa) are in the day menu, and Perlu ditangani folds into one line until opened. On the phone, screen 1 is the day with each wave as one tappable block, screen 2 one wave as cards, its button pinned above the bottom bar; the open wave and the picking screen are in the address (?wave=, &pick=1), so the browser's back closes them. Cara pakai explains every situation on the page. Isi dari picklist (typing in a filled paper picklist) is on the desktop only, also from Minta supervisor.

Test: supabase/tests/14–31, plus tests/carry-over.test.ts, tests/wave-edits.test.ts, tests/wave-fix.test.ts, tests/wave-impact.test.ts, tests/pair-moves.test.ts, tests/pickpath.test.ts.


4. Design Decisions (for the Internship Report)

Data and integrity
  • Stock only changes through the movements table. The apply_movement trigger in the database validates and changes inventory; inventory has no write policy at all, so the app, the API, and a user cannot change stock without a trace.
  • Stock is validated in the database, not in the UI. Two operators picking at the same time stay safe because the stock row is locked (FOR UPDATE) before it is decremented.
  • The ledger cannot be edited or deleted. A correction is a new movement (an adjustment), which is normal warehouse audit practice.
  • The author of a movement is forced to be the session account. The trigger fills user_id from the session, so a user_id sent by the client is ignored.
  • Opening balances and imports are recorded as adjustment. The initial stock figures can be traced back to the file they came from.
  • An empty batch is stored as '', not NULL. A unique key on (bin, SKU, batch) in Postgres does not consider two NULLs equal; '' prevents duplicates.
  • A snapshot import does not touch bins whose rows had errors. Without this, one mistyped row would reject physical stock that really exists.
  • An impossible expiry year (e.g. 1930) is imported as a warning, not rejected. The goods are physically there; rejecting the row would remove real stock from the system.
  • Remain Qty is used, not Qty. Remain Qty already accounts for the day's picks, putaways, and transfers.
Security (RLS)
  • Roles live in profiles and are checked through a has_role() SECURITY DEFINER function. A policy cannot recursively read the table it is protecting.
  • Double checking: RLS plus a role check in the function or route. If one layer is misconfigured, the other still refuses.
  • The service role key is only used on the server. It bypasses RLS, so it is never sent to the browser.
  • Public sign-up is disabled. Only the site account is used.
Labels
  • Rack column strips A–E, one 80 × 85 mm cell per level, following the sample photos. The operator finds every level from the floor without climbing.
  • QR plus a level colour band, with Code 128 optional. The QR stays readable even when the label is bent around the column; Code 128 is there for older laser scanners.
  • Barcodes are rendered as vectors, not PNG images. Module edges stay sharp on a 203 dpi printer (tested: every code read by zbar).
  • A 3 mm quiet zone is kept around the QR and Code 128. A scanner needs white space to recognise the start and end of a code.
  • Black-and-white mode. Direct thermal printers cannot print colour.
  • The PDF is built on the server. One place to log print_logs, and no dependence on what the phone can do.
Scan and UI
  • The scan field is always focused and submits on Enter. USB and Bluetooth scanners behave as keyboards, no driver needed.
  • A scan is only recorded when it comes from the scan screen (?scan=1). Refreshing the page does not inflate the scan count.
  • Every action has a confirmation step with a summary sentence. A mis-tap on the warehouse floor costs more than one extra tap.
  • A FEFO warning when a batch is chosen that is not the earliest expiry, without blocking it (sometimes there is an operational reason).
  • Cards on a phone, tables on a desktop. A 9-column table is unreadable on a 6-inch screen.
  • Bin codes are shown like the yellow location plates. What is on screen matches what the operator sees on the rack.
3D
  • One instanced mesh for 2,570 bins (one draw call), so it stays light on an office laptop.
  • Render on demand (frameloop="demand"). The GPU only works while the camera moves or the data changes, which saves tablet battery.
  • Coordinates are computed in the database from the layout configuration. Change a rack dimension once and every bin follows.
Allocation and waves, combined
  • One source of truth for stock. The old allocator's stock/stock_transactions tables were removed; the allocator reads inventory and writes through the same movements ledger. A posted pick shows up in the movement history, in ABC, and in 3D exactly like a manual movement.
  • A plan is not an execution. Saving a plan only writes waves, pick_tasks, and outbound. Stock changes when a task is posted (post_task → one movements row).
  • Stock identity = bin + SKU + batch + expiry. Rule inherited from the allocator: two expiry dates inside one batch in one bin are two stock rows, so FEFO cannot mix them up. A movement without an expiry date still works when that batch has only one row in the bin.
  • Posting is idempotent and atomic. The unique index on movements(task_id) makes double posting impossible; Selesaikan wave runs in one transaction.
  • Plans roll forward. save_plan only replaces waves that have not been worked on; running waves stay, so no pick loses its plan.
  • Reservation, not locking. Physical stock stays a single number; open tasks only reduce the stock for planning. Manual moves of reserved stock are refused for operators.
  • The actual is recorded, not guessed. A confirmation that differs from the plan needs a reason and deducts from the bin that was really used.
  • Plan tables have no write policy. Every change goes through a role-checking SECURITY DEFINER RPC, and the client cannot forge a task_id in the ledger either (RLS).
  • The allocation engine runs in the browser, exactly like the CLI (pure functions). The server only stores the result.
Inventory control
  • One policy, read by every control. inventory_policy() returns the settings key inventory_policy over built-in defaults, so shelf life, dispatch minimum, near-expiry days, count tolerance, and the approval threshold are changed in one place instead of being hard-coded per feature.
  • A hold is a status, not a move. Stock that may not ship stays where it is and keeps its identity; releasing it needs a reason.
  • Counting is blind, and a variance is recounted by someone else. The counter never sees the system quantity, and a difference outside the bin's class tolerance requires a second person — the same four-eyes idea as adjustment approval.
  • Every adjustment has a reason code from a fixed list, so root causes can be counted instead of guessed at.
  • The checker is not the poster. Receiving follows the same separation as counting: whoever checked the truck at the dock may not be the one who posts it into stock.
Audits
  • Audits are blind. The checker counts the pallet, or what is left in the bin, without seeing the system figure, so the count cannot be anchored to it.
  • The checker may not be the picker. Enforced in the database, not in the UI, and the same rule covers putaway audits.
  • A shipment cannot be loaded until every line passes. The gate is part of the wave, not a report somebody remembers to run.
  • The same audit runs against the WMS workbook when the day's picks come from the K_ONE sheet rather than from pick_tasks, so accuracy is measured the same way on both paths.
  • A count saved by mistake can be re-recorded by a supervisor, and the correction is itself recorded.
Corrections instead of new plans
  • The floor is right more often than the plan. Every correction in 0033–0055 exists because a picker found something the plan had not foreseen: short picks, stock in the wrong bin, a full destination, a growing order.
  • Corrections are separate, named actions — Batalkan posting, Pecah, Tambah Bin To Bin, Ubah baris, Tambah item — rather than hidden parameters, so the reason is obvious at the moment it is used.
  • Nothing is silently rewritten. An undone posting puts the stock back exactly as the posting took it, and both rows stay in the ledger.
Other
  • List queries are paginated at 1,000 rows. Supabase's default 1,000-row limit would truncate 2,570 bins without an error.
  • The Excel reader stops at the last non-empty cell. The WMS sheet declares a range up to row 1,048,563; reading only what exists cut the time from about 21 seconds to about 2 seconds.
  • Roles gate the navigation (components/app/nav.tsx): operator gets Scan, Penerimaan (receiving), Wave, Inventory, Gudang 3D, Cycle count, Audit picking, and Audit putaway; supervisor adds Dashboard, Alokasi (allocation), Putaway, Pickface, Adjust stok (adjust stock), Mutasi (movements), Kualitas data (data quality), Lacak batch (batch trace), Label, and Master item; admin adds Import and Pengaturan (settings).

5. Tested / Not Yet Tested

Done — in the development sandbox:

  • Migrations and seed on PostgreSQL 16 (with a stub of Supabase's auth schema): 2,570 bins, 1,790 stock rows, 52,078 in total.
  • Rules: a pick above the available stock is refused; a transfer keeps the expiry date; a transfer into a blocked bin is refused; an operator cannot adjust, import, change inventory, or delete a movement; a forged user_id is ignored; a supervisor's adjustment is recorded.
  • Import of the real WMS file: 2,614 rows in, re-import after the seed gives 0 movements.
  • Labels: exact page sizes (80 × 85 mm and 80 × 469 mm); QR and Code 128 read by zbar from the 203 dpi render.
  • tsc, ESLint, and next build pass.
  • SQL test suite: supabase/tests/00_local_auth_stub.sql–supabase/tests/31_post_as_done.sql (32 files), covering the stock rules and RLS, the allocation flow, rolling execution, putaway import, pickfaces, counts and corrections, audits, stock fixes, inventory control, picking, rack and putaway audits, the WMS-file audit, relocations, parked-wave carry-over, the pick order guard, picks found in another bin, putaway redirect, add/change relocation and pick bin, batch/expiry-aware pick-bin changes, Tambah item edits, Ubah baris, pick-only undo, Posting Bin To Bin saja, the aisle-CG block, the daily WMS report, and posting as it happened.
  • TypeScript suite: npm test runs 34 test files, 0 failures, including the FEFO engine, leftover and FEFO regression, the daily workflow, pickface behaviour, pick path, stress runs on the real workbooks, database-versus-workbook stock parity, batch-code decoding, minimum shelf life, SAP stock, and the audit exports, Tambah item edits, Perbaiki proposals, and the impact check.
  • End-to-end on local Supabase (Auth + PostgREST, supervisor and operator accounts): the 24 Sep file → 1,790 stock rows → allocation of 1,293 cartons → 8 waves / 51 tasks → all waves completed by the operator → stock 52,078 → 50,785, with every ledger row in the operator's name.

Not yet:

  • Testing against the production Supabase project (already tested on local Supabase).
  • The 3D view on a real GPU and on an operator's phone or tablet. It does render in a browser — the screenshot above was captured from a real Chromium against local Supabase, using software WebGL — but not yet on physical hardware.
  • Physical printing on the warehouse printer and scanning with an operator's phone.

6. Technical Notes

  • SheetJS: the npm xlsx@0.18.5 package has security advisories (prototype pollution / ReDoS) that are fixed in the official CDN build. Before production: npm i https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz.
  • Floor-zone labels in 3D (<Text> from drei) load a font from a CDN the first time they are opened.
  • Movement filter dates use WIB (UTC+7).
  • Light only. The app declares color-scheme: only light (app/layout.tsx, app/globals.css) and the darkreader-lock meta, so a browser or OS dark mode, or a dark-mode extension, no longer paints dropdowns and native pickers black on a white page.
  • Explicit grants (0005). New Supabase projects no longer grant SELECT/INSERT/EXECUTE to authenticated by default; without 0005 every query fails with "permission denied". Safe to run on an existing project.
  • Old allocator audit documents live in docs/archive/allocator/ and refer to code that has since been replaced.
  • The migration set is the real index of what exists: supabase/migrations/0001_schema.sql–supabase/migrations/0060_post_wave_sheet_final.sql, with the test covering each range named in section 3.

About

WMS CKB Surabaya — bin labels, scanning, 3D stock, FEFO allocation, picklists & wave execution on one Supabase ledger

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages