From 7ad995d8d158b79da919d378da1a3e70577c59f5 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 2 Oct 2026 15:10:13 +0700 Subject: [PATCH] feat(manual-batch): require plate number at manual mode start Legacy manual batch start locked until plate entered: - POST /api/batch/start manual branch returns 400 missing_plate when plate empty (strip + uppercase, no format regex) - operator + monitoring start modals gain required plate input, confirm button disabled until filled - operator plate tile also shows for manual batches (was do_manual only) - do_manual unchanged (plate from DOs) --- CHANGELOG.md | 7 +++++++ counter_dashboard.py | 11 +++++++++++ docs/configuration.md | 2 +- templates/monitoring.html | 37 ++++++++++++++++++++++++++++++++++--- templates/operator.html | 32 ++++++++++++++++++++++++++++++-- tests/test_do_batch.py | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 118 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c669724..5d074b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ version tags are cut in this repo — POC stage, `main` is the release line). ## [Unreleased] ### Added +- **Manual mode plate entry**: legacy `manual` batch start is now locked until a + plate number is entered — plate input in operator + monitoring start modals + (confirm button disabled while empty), `POST /api/batch/start` rejects + manual start with 400 `missing_plate` (strip + uppercase, no format regex); + plate flows through `current_batch.json` → `batches.plate` on stop, so + history/XLSX show it for manual batches. Operator plate tile now also shows + for manual batches (was `do_manual` only). - **DO-gated manual batch mode** (`auto` default · `do_manual` · legacy `manual`): smartphone photo capture → OCR draft → plate-grouped start gates → net expected/counted on live panel → stop soft-warn + force → discard only when diff --git a/counter_dashboard.py b/counter_dashboard.py index dbad551..4022dda 100644 --- a/counter_dashboard.py +++ b/counter_dashboard.py @@ -472,6 +472,17 @@ def api_batch_start(): expected_sack = sum(int(d.get("expected_sack") or 0) for d in dos) expected_box = sum(int(d.get("expected_box") or 0) for d in dos) + elif mode == "manual": + # Legacy manual mode: batch start locked until plate entered. + req_data = request.get_json(silent=True) or request.form or {} + plate = str(req_data.get("plate", "")).strip().upper() + if not plate: + return jsonify({ + "success": False, + "reason": "missing_plate", + "error": "Plat nomor wajib diisi sebelum batch dimulai.", + }), 400 + counting_date = get_counting_date() batch_num = get_next_batch_number(counting_date) now_iso = datetime.now().isoformat() diff --git a/docs/configuration.md b/docs/configuration.md index 0078647..52763b4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -38,7 +38,7 @@ deprecated (warned). |---|---|---| | `auto` (default) | AI truck FSM | n/a | | `do_manual` | Operator start/stop + staged DO photos | yes | -| `manual` | Operator start/stop (legacy) | no | +| `manual` | Operator start/stop (legacy) — plate required at start | no | `POST /api/batch/mode` with `mode` and/or `model_mode` is **office port only** (403 on operator `:5000`); mode switch while a batch is active → **409**. diff --git a/templates/monitoring.html b/templates/monitoring.html index cb52f8f..879e332 100644 --- a/templates/monitoring.html +++ b/templates/monitoring.html @@ -405,10 +405,16 @@ @@ -479,7 +485,21 @@ } } + function onMonStartPlateInput() { + const plate = document.getElementById('monStartPlateInput').value.trim(); + const btn = document.getElementById('monBtnStartConfirm'); + btn.style.opacity = plate.length === 0 ? '0.6' : ''; + btn.style.pointerEvents = plate.length === 0 ? 'none' : ''; + } + function confirmStartBatch() { + const plateRow = document.getElementById('monStartPlateRow'); + const isManual = currentBatchMode === 'manual'; + plateRow.style.display = isManual ? 'block' : 'none'; + if (isManual) { + document.getElementById('monStartPlateInput').value = ''; + onMonStartPlateInput(); + } document.getElementById('modalStart').style.display = 'flex'; } @@ -490,7 +510,18 @@ async function executeStartBatch() { document.getElementById('modalStart').style.display = 'none'; try { - const res = await fetch('/api/batch/start', { method: 'POST' }); + let body = null; + let headers = {}; + if (currentBatchMode === 'manual') { + const plate = document.getElementById('monStartPlateInput').value.trim().toUpperCase(); + if (!plate) { + alert('Plat nomor wajib diisi sebelum batch dimulai.'); + return; + } + body = JSON.stringify({ plate: plate }); + headers = { 'Content-Type': 'application/json' }; + } + const res = await fetch('/api/batch/start', { method: 'POST', headers: headers, body: body }); const data = await res.json(); if (data.success) { await pollCurrentBatch(); diff --git a/templates/operator.html b/templates/operator.html index 6d6fb51..62fbfe7 100644 --- a/templates/operator.html +++ b/templates/operator.html @@ -118,6 +118,7 @@ .btn-disabled { opacity: 0.6; cursor: not-allowed; + pointer-events: none; } /* Simple Confirmation Modal */ @@ -425,10 +426,16 @@ @@ -687,10 +694,19 @@ return (data && data.items) || []; } + function onStartPlateInput() { + const plate = document.getElementById('startPlateInput').value.trim(); + const btn = document.getElementById('btnStartConfirm'); + btn.classList.toggle('btn-disabled', plate.length === 0); + } + function confirmStartBatch() { const gate = document.getElementById('startGateList'); const desc = document.getElementById('startModalDesc'); + const plateRow = document.getElementById('startPlateRow'); if (currentMode === 'do_manual') { + plateRow.style.display = 'none'; + document.getElementById('btnStartConfirm').classList.remove('btn-disabled'); fetchStagedForStart().then(items => { stagedDos = items; const problems = []; @@ -717,6 +733,9 @@ } gate.style.display = 'none'; desc.textContent = 'Pastikan truk sudah siap di posisi pemuatan karung.'; + document.getElementById('startPlateInput').value = ''; + plateRow.style.display = 'block'; + onStartPlateInput(); document.getElementById('modalStart').style.display = 'flex'; } @@ -744,6 +763,14 @@ if (currentMode === 'do_manual') { const items = await fetchStagedForStart(); body = JSON.stringify({ do_ids: items.map(d => d.id) }); + } else if (currentMode === 'manual') { + const plate = document.getElementById('startPlateInput').value.trim().toUpperCase(); + if (!plate) { + closeModal('modalStart'); + alert('Plat nomor wajib diisi sebelum batch dimulai.'); + return; + } + body = JSON.stringify({ plate: plate }); } const res = await fetch('/api/batch/start', { method: 'POST', @@ -817,7 +844,8 @@ const plateItem = document.getElementById('opPlateItem'); const plateDos = document.getElementById('opPlateDos'); const batchActive = !!(data.success && data.batch_number); - const showPlate = currentMode === 'do_manual' && batchActive; + const showPlate = batchActive && + (currentMode === 'do_manual' || !!(data.plate && data.plate.trim())); if (doSack) doSack.textContent = data.expected_sack || 0; if (doBox) doBox.textContent = data.expected_box || 0; if (plateItem) plateItem.style.display = showPlate ? 'block' : 'none'; diff --git a/tests/test_do_batch.py b/tests/test_do_batch.py index 2e54e75..a860cbb 100644 --- a/tests/test_do_batch.py +++ b/tests/test_do_batch.py @@ -266,6 +266,41 @@ def test_start_stop_auto_409(dash_client): assert res.status_code == 409 +def test_manual_start_missing_plate_400(dash_client): + client, mode_path, state_path, _ = dash_client + office = {"Host": f"localhost:{cd_office_port()}"} + client.post("/api/batch/mode", json={"mode": "manual"}, headers=office) + res = client.post("/api/batch/start", headers={"Host": "localhost:5000"}) + assert res.status_code == 400 + body = res.get_json() + assert body["success"] is False + assert body["reason"] == "missing_plate" + assert not state_path.exists() + + res = client.post("/api/batch/start", json={"plate": " "}, + headers={"Host": "localhost:5000"}) + assert res.status_code == 400 + assert res.get_json()["reason"] == "missing_plate" + assert not state_path.exists() + + +def test_manual_start_with_plate_normalizes_and_persists(dash_client): + client, mode_path, state_path, _ = dash_client + office = {"Host": f"localhost:{cd_office_port()}"} + client.post("/api/batch/mode", json={"mode": "manual"}, headers=office) + res = client.post("/api/batch/start", json={"plate": " b 1234 xyz "}, + headers={"Host": "localhost:5000"}) + assert res.status_code == 200 + body = res.get_json() + assert body["success"] is True + assert body["plate"] == "B 1234 XYZ" + assert state_path.exists() + state = json.loads(state_path.read_text()) + assert state["plate"] == "B 1234 XYZ" + assert state["batch_mode"] == "manual" + assert state["manual_control"] is True + + def test_stop_discard_gross_zero(dash_client): client, mode_path, state_path, _ = dash_client office = {"Host": f"localhost:{cd_office_port()}"}