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.
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.
| 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.
| Area | What it does |
|---|---|
| Labels | Rack 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 |
| Scan | Phone camera or USB/Bluetooth scanner, always focused, submits on Enter |
| 3D warehouse | 2,570 bins in one draw call, rendered on demand, colour by ABC, utilisation or expiry |
| FEFO allocation | From the database or the day's WMS file; one bin-choice rule; picklists per shipment, forklift and handpick apart; serpentine pick path |
| Waves | Idempotent 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)) |
| Corrections | Isi 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 things | Tap 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 & pickfaces | Sheet import with per-row conflict decisions; one fixed pick bin per SKU |
| Inventory control | Holds, reason codes, four-eyes approval, blind cycle counts, receiving, SAP reconciliation, FEFO compliance, Laporan WMS harian (on hand and remain per day) |
| Audits | Blind 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
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.
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 | warehouse mapping status table and was imported as active0054) 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.
- Create a project at supabase.com (Singapore region).
- Apply the migrations in filename order. The set is 60 files,
supabase/migrations/0001_schema.sqlthroughsupabase/migrations/0060_post_wave_sheet_final.sql; use the CLI rather than the SQL Editor so nothing is skipped or run twice:A project that is already running (0001–0003 applied): apply everything fromsupabase link --project-ref <ref> supabase db push
0004onwards. Section 3 maps each range of migrations to the feature it delivers. Two worth knowing about:supabase/migrations/0015_realtime.sqlturns 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.sqlchanges the stock identity to bin + SKU + batch + expiry, and old data stays valid. - 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. - Site account: create one account under Authentication → Users → Add user (name it e.g. Gudang), then in the SQL Editor:
Fill in
update public.profiles set role = 'admin', name = 'Gudang' where id = (select id from auth.users where email = '<site account email>');
SITE_ACCOUNT_EMAILandSITE_ACCOUNT_PASSWORDin the hosting environment (Vercel) and in.env.local. WhileLOGIN_REQUIREDis nottrue, every visitor shares this account session, so the app opens straight away. On ckb-wms-v2 migration0071names that accountgudangand0072marks 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 pushand deploy withLOGIN_REQUIREDunset — nothing changes; (2) sign in at/loginasgudangwith the site-account password, create everyone's accounts onPenggunaand hand out usernames and starting passwords; (3) setLOGIN_REQUIRED=truein Vercel and redeploy — devices still on the shared account are sent to/loginon their next page; (4) changegudang's password (Reset kata sandi) and removeSITE_ACCOUNT_EMAIL/SITE_ACCOUNT_PASSWORD. Until step 3, personal accounts protect nothing: anyone can be the shared admin, andPenggunawill not change the shared account. Before step 3, check the user list: every admin created meanwhile keeps admin rights. - 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.xlsxFor routine updates use the Import menu in the app (recorded as movements), not the seed.
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), sameOr all of them at once against a temporary local database: scripts/sql-test.sh.
cp .env.example .env.local # fill in the URL, anon key, service role key (Project Settings → API)
npm install
npm run dev # http://localhost:3000Phone 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.
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).
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.
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.
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/.
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.
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.
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.
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 themovementsledger.- 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.
- Posting →
- 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
Alokasiagain 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.
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 adjustmentHITUNG <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 theWavepage with a recalculate button. - Bin scan: the bin page shows the planned tasks that take from or fill that bin.
- Stock ledger:
Mutasiis 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.
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.
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.
0017Stock 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.0018Adjustment control — every movement carries areason_codefrom a fixed list, the name typed on the floor, and a four-eyes approval for movements above the threshold.0019Blind counts — the counter never sees the system quantity; a count outside the bin's class tolerance goes toRECOUNTand a different person counts it.0020Receiving (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.0021SAP 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.0022FEFO 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.0023Pick 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.
0024Picking 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.0025Rack 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.0026fixes the accuracy KPIs: a wave cancelled after some of its lines were audited no longer counts them.0027Putaway audit at the rack — per putaway the checker scans or types the SKU, types its batch, and counts it; the database compares all three.0028lets operators be the checker on putaway audits; the named checker may not be the person who did the putaway.0029and0030run the same two audits against the WMS workbook instead ofpick_tasks, for the days when the K_ONE picklist is the source of truth.0031lets the checker record a wrong item found in the bin, which becomes its own follow-up.0032lets 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.
Everything here exists because of something that happened on the floor; each migration header records the real case.
-
0033a relocation carries what is really left in the bin, not what the plan assumed, when a broken pallet is picked short. -
0034Batalkan postingundoes 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. -
0035a pick and its pallet's leftover move post as one line on the wave page, the way the picklist prints them. -
0036and0037a 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. -
0038picks are worked in plan order, and a pick that finds less than planned becomes a count instead of silently taking stock from elsewhere. -
0039a pick whose cartons are physically in another bin than the books say can be posted from that bin (Bin lain). -
0040and0041SKUs 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 nextAlokasi; volume is the litres per unit. -
0042a putaway sheet row whose bin holds other stock can be sent to the bin the pallet really went to (Pindah ke bin lain). -
0043Bin kosonglists 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. -
0044Tambah orderallocates a late order straight onto a day without the Schedule of the day sheet and without re-runningAlokasi. -
0045Tambah Bin To Binadds a move for what a pick leaves behind when the plan had none;0047Ubah Bin To Binsends an open move to a different destination. -
0046Pecahsplits an open pick in two so each part can be posted from its own bin. -
0048Tambah itemallocates more cartons of a SKU, or a new SKU, onto a wave that already exists. -
0049Ubah Bin Picklets a supervisor re-point an open pick to a different source bin, and the pairedBin To Bintakes the same source while the move's destination and the pick's batch, expiry and quantity are never changed. -
0050the 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 instok kurang. -
0051Tambah itemrows can follow the printed picklist before saving: any source bin (a later expiry is a warning, not a block), anyBin To Bindestination 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. -
0052Ubah barisreplacesUbah Bin Pick,Tambah Bin To BinandUbah Bin To Binon open rows: one dialog sets the pick's source (any bin, any batch; a later expiry is warned) and itsBin To Bin(change, add or remove) in one transaction with one name, reason and log line. An open row shows onlyPostingand⋯;Ubah baris,PecahandBatalsit behind⋯, andBatalno longer asks for confirmation (Pulihkanundoes it). -
0053Posting Bin To Bin sajaposts one open move without its pick, also on a wave inTunda, 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 postingon such a row can also undo the pick alone (Pick saja) when the rest in the pickface is already used by other waves. -
0054aisle CG does not exist: its 400 template bins from the WMS sheet are blocked, so they leaveBin kosongand are refused as targets; a later import keeps them blocked. -
0055Laporan WMS harianrebuilds, 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 inon every row, an undone pick counted as un-picking. Downloads as Excel with a per-SKU sheet for SAP. -
0056Posting sesuai lapangantakes 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. -
0057Isi 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, orBelumto leave a row open. Rows already posted can be corrected in the same sheet. OneSimpan & postingposts 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 aFOUNDadjustment (Koreksi picklist) with a count task on the bin. Every such correction is listed underKoreksi pickliston 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, withAdjustper line andUnduh Excel. -
0058Cek sisa bin: after a phone Posting that leaves at mostbin_check_max_qtycartons (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 inbin_checks; one that differs from the system opens a count task on the bin. Stock is not changed by the answer. -
0059Putaway of a pallet rest already moved in: adata putawayrow into a bin that received the same SKU by Bin To Bin in the last two days is amoved_inconflict (shown with that move). It posts only withPalet baru: tetap posting; left alone it becomes a count task, so a pallet rest is never counted twice. -
0060re-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. -
0066Sheet benar: betulkan isi binonPutaway: 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)orPindah ke bin— then the sheet's pallet is put away. Moves post as transfers; adjustments withinadjust_approval_qtypost asCOUNT_VARIANCE, larger ones wait inAdjust stokfor 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. -
0067Paperless picking —Mulai pickingon every running wave opens the wave line by line on the phone, in picklist order, for every role. Per lineSesuaiposts the plan,Berbedaposts what happened:Qtyin 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_lineposts it through theIsi dari picklistengine (post_wave_sheet_rows) with the picker's limits: a name, at most the order line, aKoreksi picklistonly up toadjust_approval_qty, andUbahonly on one's own line while the wave runs and the line is neither audited nor loaded. Anything beyond that is recorded inpick_line_requestsand listed underPerlu ditangani · Minta supervisor, whereIsi dari picklistopens at that line with what the picker posted. The engine also no longer books a pallet rest again as aKoreksi picklistwhen that rest was already moved withPosting 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. -
0068Rack 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.eanis empty). A wrong label is refused before anything posts:Bin salah: kamu scan X, baris ini Y. Each completed task recordsscan_method(scan/ketik) and the confirmed code;unpost_taskclears them. -
0069Buka lagi(a finished wave) andPulihkan wave(a cancelled one), supervisor and admin, with a name and a reason.Buka lagisets the wave and its orders running again and offers to undo the linesSelesaikan waveposted 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 wavereturns 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 offersHitung ulang wave tersisa. Neither changesRemain Qtyexcept by those reversing movements. -
0070fefo_exceptions(the Dashboard'sFEFO compliancetile) 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. -
0071Personal accounts (login, not required yet:LOGIN_REQUIRED):profiles.username/active, admin-onlyset_user_role/set_user_active,account_emailfor 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. -
0072marks the shared site account (app_metadata.shared): with login required its sessions are signed out; with login not requiredPenggunaleaves 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 andUnduh 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 picklistorMutasi, 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.
Data and integrity
- Stock only changes through the
movementstable. Theapply_movementtrigger in the database validates and changesinventory;inventoryhas 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_idfrom the session, so auser_idsent 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
profilesand are checked through ahas_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_transactionstables were removed; the allocator readsinventoryand writes through the samemovementsledger. 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, andoutbound. Stock changes when a task is posted (post_task→ onemovementsrow). - 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 waveruns in one transaction. - Plans roll forward.
save_planonly 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_idin 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 keyinventory_policyover 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–0055exists 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):operatorgetsScan,Penerimaan(receiving),Wave,Inventory,Gudang 3D,Cycle count,Audit picking, andAudit putaway;supervisoraddsDashboard,Alokasi(allocation),Putaway,Pickface,Adjust stok(adjust stock),Mutasi(movements),Kualitas data(data quality),Lacak batch(batch trace),Label, andMaster item;adminaddsImportandPengaturan(settings).
Done — in the development sandbox:
- Migrations and seed on PostgreSQL 16 (with a stub of Supabase's
authschema): 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 forgeduser_idis 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, andnext buildpass. - 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 testruns 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.
- SheetJS: the npm
xlsx@0.18.5package 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 thedarkreader-lockmeta, 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 toauthenticatedby default; without0005every 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.
