feat: operator Sacks/Boxes/DO tiles, drop Net from history
ci / smoke (push) Canceled after 0s

This commit is contained in:
andrew committed 2026-09-25 08:52:29 +07:00
1 parent 0efcfa45b8
commit 1e4dc4201c
7 files changed
+172 -729

No files matched your search

+1 -2
View File
@@ -102,7 +102,6 @@ POST). **DO manual flow:** open `http://<jetson>:5000/operator` on a phone →
Ambil Foto DO → review/edit OCR fields → Mulai Batch → count → Selesai Ambil Foto DO → review/edit OCR fields → Mulai Batch → count → Selesai
(soft-warn if zone busy). Discard only when both sack and box nets are 0. (soft-warn if zone busy). Discard only when both sack and box nets are 0.
ERD: [`docs/do-erd.md`](docs/do-erd.md). ERD: [`docs/do-erd.md`](docs/do-erd.md).
Plan: [`docs/do-batch-implementation-plan.md`](docs/do-batch-implementation-plan.md).
## Repository layout ## Repository layout
@@ -133,6 +132,6 @@ Script-by-script reference: [`docs/scripts.md`](docs/scripts.md).
- [`docs/configuration.md`](docs/configuration.md) — all config files/variables - [`docs/configuration.md`](docs/configuration.md) — all config files/variables
- [`docs/deployment.md`](docs/deployment.md) — Jetson services, TensorRT, deploy flow - [`docs/deployment.md`](docs/deployment.md) — Jetson services, TensorRT, deploy flow
- [`docs/do-erd.md`](docs/do-erd.md) — delivery-order / batch ERD (Mermaid) - [`docs/do-erd.md`](docs/do-erd.md) — delivery-order / batch ERD (Mermaid)
- [`docs/do-batch-implementation-plan.md`](docs/do-batch-implementation-plan.md) — DO manual batch plan - [`docs/operator-tiles-history-plan.md`](docs/operator-tiles-history-plan.md) — operator tiles + history cleanup plan
- [`docs/scripts.md`](docs/scripts.md) — entry points & utility scripts - [`docs/scripts.md`](docs/scripts.md) — entry points & utility scripts
- [`CHANGELOG.md`](CHANGELOG.md) — release history - [`CHANGELOG.md`](CHANGELOG.md) — release history
+10 -13
View File
@@ -1703,10 +1703,9 @@ def export_day_xlsx(date):
ws = wb.active ws = wb.active
ws.title = f"Day {date}" ws.title = f"Day {date}"
_style_header(ws, [ _style_header(ws, [
("A", "Batch Number"), ("B", "Count"), ("C", "Box"), ("D", "Net Sack"), ("A", "Batch Number"), ("B", "Sacks"), ("C", "Boxes"),
("E", "Net Box"), ("F", "Plate"), ("G", "DO"), ("H", "Expected Sack"), ("D", "Plate"), ("E", "DO"), ("F", "DO Sacks"), ("G", "DO Boxes"),
("I", "Expected Box"), ("J", "Start Time"), ("K", "End Time"), ("H", "Start Time"), ("I", "End Time"), ("J", "Duration (min)"),
("L", "Duration (min)"),
]) ])
for r_idx, row in enumerate(rows, 2): for r_idx, row in enumerate(rows, 2):
@@ -1717,15 +1716,13 @@ def export_day_xlsx(date):
ws.cell(row=r_idx, column=1, value=row["batch_number"]) ws.cell(row=r_idx, column=1, value=row["batch_number"])
ws.cell(row=r_idx, column=2, value=row["count"]) ws.cell(row=r_idx, column=2, value=row["count"])
ws.cell(row=r_idx, column=3, value=row["box_loading"]) ws.cell(row=r_idx, column=3, value=row["box_loading"])
ws.cell(row=r_idx, column=4, value=row["net_sack"] if "net_sack" in row.keys() else row["count"]) ws.cell(row=r_idx, column=4, value=row["plate"] if "plate" in row.keys() else "")
ws.cell(row=r_idx, column=5, value=row["net_box"] if "net_box" in row.keys() else row["box_loading"]) ws.cell(row=r_idx, column=5, value=", ".join(do_numbers))
ws.cell(row=r_idx, column=6, value=row["plate"] if "plate" in row.keys() else "") ws.cell(row=r_idx, column=6, value=row["expected_sack"] if "expected_sack" in row.keys() else 0)
ws.cell(row=r_idx, column=7, value=", ".join(do_numbers)) ws.cell(row=r_idx, column=7, value=row["expected_box"] if "expected_box" in row.keys() else 0)
ws.cell(row=r_idx, column=8, value=row["expected_sack"] if "expected_sack" in row.keys() else 0) ws.cell(row=r_idx, column=8, value=row["start_time"])
ws.cell(row=r_idx, column=9, value=row["expected_box"] if "expected_box" in row.keys() else 0) ws.cell(row=r_idx, column=9, value=row["end_time"])
ws.cell(row=r_idx, column=10, value=row["start_time"]) ws.cell(row=r_idx, column=10, value=row["duration_minutes"] or 0)
ws.cell(row=r_idx, column=11, value=row["end_time"])
ws.cell(row=r_idx, column=12, value=row["duration_minutes"] or 0)
_auto_width(ws) _auto_width(ws)
filename = f"{SITE_NAME}_day_detail_{date}.xlsx" filename = f"{SITE_NAME}_day_detail_{date}.xlsx"
+1 -1
View File
@@ -147,7 +147,7 @@ dashboard integration is file-based.)
DO pipeline (dashboard): smartphone upload → OCR draft → edit/stage → start DO pipeline (dashboard): smartphone upload → OCR draft → edit/stage → start
gates → active batch → stop-preview soft-warn → stop → `batches` + DO columns. gates → active batch → stop-preview soft-warn → stop → `batches` + DO columns.
ERD: [`docs/do-erd.md`](do-erd.md). Plan: [`docs/do-batch-implementation-plan.md`](do-batch-implementation-plan.md). ERD: [`docs/do-erd.md`](do-erd.md). UI plan: [`docs/operator-tiles-history-plan.md`](operator-tiles-history-plan.md).
## Retired (`archive/`, not imported) ## Retired (`archive/`, not imported)
-684
View File
@@ -1,684 +0,0 @@
# Implementation Plan — DO-Gated Manual Batch Counting
> Status: **implemented** (Phase 1–5 code + docs + tests; manual device checks + Jetson deploy pending).
> Scope: **karung feedmill counter only**.
> pfm-ocr (`git.proit.id/andrew/pfm-ocr`) is **inspiration only** — no API/DB coupling.
## 1. Goal
**Default runtime mode: auto batching** (existing truck/sack FSM — unchanged).
**Office** (port 5721) switches mode to DO-gated manual or legacy manual; operator
(port 5000) only **views** current mode and acts within it.
DO-gated manual workflow (when selected) — **photos taken with a smartphone**
via the operator dashboard page (browser `capture="environment"` on the phone):
```
smartphone photo DO(s) → review OCR → upload/stage → start batch → count
→ truck leaves → operator ends batch (soft warn + force)
```
- One truck may carry **multiple DOs**.
- DOs with the **same vehicle plate** belong to **one batch**.
- Same plate may return **more than once per day** — each visit is a new batch
delimited by operator start/stop, not by plate uniqueness.
- Truck detect / ROI / overlay stay **live as today** in all modes.
- Auto mode: Jetson owns truck presence / batch open-close (existing behaviour).
## 2. Locked decisions
| # | Topic | Decision |
|---|---|---|
| 1 | Batch discard | Discard only if **both** net karung **and** net kardus are 0 (`net_sack == 0 and net_box == 0`). Keep the row if either side is non-zero. |
| 2 | Expected vs counted | **Net** = `loading − unloading` for sacks and for boxes |
| 3 | OCR | **Light CPU OCR first** (Tesseract); **PaddleOCR-level backup** same `extract_do_fields()` dispatch. **Runtime engine** (`tesseract`\|`paddle`\|`none`) in `do_settings.json` — **toggleable from office (5721) and operator (5000)**; both UIs read/write the same store and stay in sync via `GET /api/do/settings` poll. YAML `do.ocr.engine` = seed only |
| 4 | Photo retention | **7 days** (`do.retention_days`), purge photos + draft/staged DO rows |
| 5 | `require_plate` toggle | **Office port only** (`is_office_request()`, default 5721) |
| 6 | Capture UI | Operator dashboard (`templates/operator.html`, port 5000). **Capture device: smartphone** browser camera (`accept="image/*" capture="environment" multi`); no native app, no desktop webcam required |
| 7 | Grouping | Auto-bucket staged DOs by plate; start **blocks** mixed non-empty plates |
| 8 | Plate gate | Runtime toggle: OFF = plate optional; ON = all attached DOs need plate |
| 9 | DO required to start | `do.require_do: true` (locked OK) — start needs ≥1 staged DO in **do_manual** |
| 10 | Storage | `jetson_counter.db` + local photo files under output dir |
| 11 | **Batch modes** | **3 modes**: **`auto` (default)** · `do_manual` (DO-gated flow) · `manual` (legacy start/stop, no DO gate — dev/fallback). **Mode switch POST: office port only**; GET mode: both ports |
| 11b | **Model mode (A–D)** | **Office only** (`is_office_request()`). **Remove** model-mode selector from `operator.html`; only `monitoring.html` (office) can POST `model_mode` |
| 12 | End-batch guard | Soft warn if activity in counting zone → **force** still allowed (do_manual stop; auto unchanged) |
### 2.1 Batch mode matrix
| Mode | Who opens/closes batch | DO gate | Notes |
|---|---|---|---|
| **`auto` (default)** | Existing FSM in `predict.py` / `src/batch.py` | n/a | **No behaviour change** vs today. Operator start/stop buttons hidden or inert. DO panel hidden. |
| **`do_manual`** | Operator start/stop only | Yes (`do.require_do`, plate rules) | New flow in this plan. Truck FSM ignored for open/close; ROI/truck detect still live. |
| **`manual`** | Operator start/stop only | No (legacy) | Existing manual buttons + `current_batch.json`; kept for dev experiments. |
- Persist mode in **`batch_mode.json`** key `mode` (extend allowed values from
`auto|manual` → `auto|do_manual|manual`).
- **Default when file missing / invalid: `auto`.**
- `GET /api/batch/mode` — both ports (operator UI polls for badge / DO panel).
- `POST /api/batch/mode {"mode": ...}` — **office only** (`is_office_request()`);
operator POST → **403** (blocks `switchToManual()` on port 5000 as-is).
- Allowed values `auto|do_manual|manual`; unknown → 400.
- Mode switch while a batch is active: **409** until batch stopped —
avoids mixing auto-finalize with DO columns mid-batch.
- **`model_mode` (A–D)** on same endpoint: **office only** as well (locked #11b).
Operator must not POST either `mode` or `model_mode`. Office monitoring page
owns both switches; operator UI reads active model mode for badge only.
- `config.yaml` seed: `batch.default_mode: auto` (file-less first boot); runtime
still `batch_mode.json`.
## 3. Non-goals
- No pfm-ocr network auth, Postgres, or Flutter dependency.
- No change to YOLO modes A–D, counting line geometry, or batch FSM logic in `src/batch.py`.
- No auto open/close of batches from truck detector while mode is `do_manual` / `manual`.
- No multi-tenant / store accounts.
- No change to default production behaviour beyond **default mode string**
(`auto` remains the out-of-box runtime mode).
---
## 4. Architecture
Mode selects path; only **`do_manual`** uses the DO pipeline below:
```
batch_mode.json mode
├─ auto (default) ──► predict.py existing FSM (no DO APIs required)
├─ manual ──────────► operator start/stop, no DO gate (legacy)
└─ do_manual ───────► DO pipeline:
operator.html (5000)
│ multi-photo capture (panel visible only when mode == do_manual)
▼
POST /api/do/upload ──► save JPEG under {output.dir}/do_photos/YYYY-MM-DD/
│ OCR extract (tesseract|paddle) → draft row
▼
review/edit fields ──► PUT /api/do/<id> (no_do, plate, expected_sack, expected_box)
│
▼
POST /api/batch/start {do_ids} ── validates gates ──► current_batch.json
│ + delivery_orders.status=attached
▼
predict.py manual branch (mode != auto) ── updates count/box_count in state file
│
▼
GET /api/batch/stop-preview ── zone busy? ──► modal warn
POST /api/batch/stop ── finalize ──► batches + plate/do_numbers/expected_*
└── net_sack==0 AND net_box==0 → discard (no row)
```
### Expected qty (net)
- Per DO line items (after OCR or manual entry), classify unit as sack-like
vs box-like (matched case-insensitive on the qty/unit token):
| Class | Terms (add more if DOs use other spellings) |
|---|---|
| sack-like | `KRG`, `KARUNG`, `SACK`, `BG` |
| box-like | `BOX`, `CTN`, `KARTON`, **`DUS`**, **`KARDUS`** |
- Store `expected_sack`, `expected_box` as **integer outer units**.
- Live variance on operator panel:
- sacks: `(loading_count − unloading_count)` vs `expected_sack`
- boxes: `(box_loading − box_unloading)` vs `expected_box`
- Same net formula written into history/export.
### Discard rule (stop)
- Compute `net_sack` and `net_box` at stop (see §7.3).
- **Discard** (no `batches` insert) only when `net_sack == 0` **and** `net_box == 0`.
- If either net is non-zero → **insert** the batch row (DO columns included), even
when the other class is zero (e.g. boxes only, no sacks).
---
## 5. Data model
### 5.1 New table `delivery_orders`
```sql
CREATE TABLE IF NOT EXISTS delivery_orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
counting_date TEXT NOT NULL,
photo_path TEXT NOT NULL, -- relative to output.dir
no_do TEXT NOT NULL DEFAULT '',
plate TEXT NOT NULL DEFAULT '',
expected_sack INTEGER NOT NULL DEFAULT 0,
expected_box INTEGER NOT NULL DEFAULT 0,
ocr_raw TEXT, -- JSON: engine, raw text, confidence
status TEXT NOT NULL DEFAULT 'draft', -- draft|staged|attached|discarded
batch_id INTEGER, -- set when attached (informational)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_do_date_status
ON delivery_orders(counting_date, status);
```
`counting_date` = same helper as batches (`get_counting_date()`), so photos
taken after cutoff belong to the operational day.
### 5.2 `batches` additive columns
```sql
-- same PRAGMA table_info pattern as box_loading/model_mode migration
plate TEXT NOT NULL DEFAULT '',
do_numbers TEXT NOT NULL DEFAULT '[]', -- JSON array of No. DO
expected_sack INTEGER NOT NULL DEFAULT 0,
expected_box INTEGER NOT NULL DEFAULT 0,
net_sack INTEGER NOT NULL DEFAULT 0, -- loading - unloading at stop
net_box INTEGER NOT NULL DEFAULT 0
```
Discard stop only when both nets are 0 → no row. Otherwise expected/net
columns are written with the batch.
### 5.3 `current_batch.json` extra keys
```json
{
"manual_control": true,
"plate": "B 1234 XYZ",
"do_ids": [12, 13],
"do_numbers": ["DO-001", "DO-002"],
"expected_sack": 120,
"expected_box": 10
}
```
`predict.py` already round-trips the whole dict; **no structural change** unless
finalize moves into predict (it does not — stop stays in dashboard).
### 5.4 Photo tree
```
{output.dir}/do_photos/
2026-09-24/
do_<uuid>.jpg
```
Source: **smartphone** camera upload (see §9.1); server downscales before write.
Gitignored via existing `*.jpg` rule. Retention deletes files + rows ≤ now−7d.
ERD (Mermaid) ships in **Phase 4** → `docs/do-erd.md`.
---
## 6. Configuration
### 6.1 `config.yaml`
```yaml
# do = Delivery Order (surat jalan). Gated manual-batch scan flow.
# Used when batch mode == do_manual (mode itself lives in batch_mode.json).
batch:
default_mode: "auto" # auto | do_manual | manual (seed when batch_mode.json missing)
do:
enabled: true
require_do: true # start needs ≥1 staged DO (do_manual mode)
require_plate: false # default; office UI can flip at runtime
retention_days: 7
photo_dir: "do_photos" # relative to output.dir
max_photos_per_batch: 8
zone_warn_seconds: 3 # stop soft-warn: recent count activity
ocr:
engine: "tesseract" # tesseract | paddle | none — YAML seed only
# Runtime override: do_settings.json ocr_engine (UI toggle, both ports).
# paddle = PaddleOCR-level backup (same extract_do_fields dispatch);
# use when tesseract field accuracy fails acceptance.
```
Note: existing `batch:` key already holds `timeout_seconds` / `daily_cutoff_time` —
**add** `default_mode` under it; do not duplicate the `batch:` key.
### 6.2 Runtime settings (not config.yaml)
| Store | Contents | Writer |
|---|---|---|
| `{output.dir}/batch_mode.json` | `mode`: `auto` \| `do_manual` \| `manual` (**default `auto`**) | **`POST /api/batch/mode` office only** |
| `{output.dir}/do_settings.json` | `require_plate`, `require_do` (office-only write); **`ocr_engine`** (`tesseract`\|`paddle`\|`none`, **both ports write**) | See §7 settings auth |
- Existing `_read_batch_mode_file()` defaults change: missing file →
`mode = CFG.batch.default_mode` or `"auto"` (not `"manual"`).
- Invalid/legacy values: map unknown → `auto` + warn; keep ignoring legacy
`model_mode` key as today.
- Mode switch API: `mode not in ["auto", "do_manual", "manual"]` → 400;
`not is_office_request()` on POST when `mode` in body → **403**.
- POST with **`mode` and/or `model_mode`**: **office only** (403 on operator port
when either write field is present). GET unchanged on both ports.
Auth on settings writes:
- `POST /api/do/settings` **`ocr_engine`**: **both ports allowed** (operator and
office); same file → both UIs stay in sync on next `GET`.
- `POST /api/do/settings` **`require_plate` / `require_do`**: **403** if not
`is_office_request()`.
- `POST /api/batch/mode` with `mode` (and/or `model_mode`): **403** if not
`is_office_request()`.
- Unknown `ocr_engine` value → 400; missing deps for `paddle` still explicit
error on next upload (§8), not on settings write.
### 6.3 `src/config_loader.py`
Add `DoConfig` + `DoOcrConfig` dataclasses; parse `do:` block; defaults if key absent
(missing-file behaviour unchanged).
---
## 7. Backend API (`counter_dashboard.py`)
| Method | Path | Auth / port | Behaviour |
|---|---|---|---|
| `GET` | `/api/batch/mode` | both | Effective mode (`auto` \| `do_manual` \| `manual`) + `mode_editable` |
| `POST` | `/api/batch/mode` | **office only** when body has `mode` and/or `model_mode` | Persist; **403** if operator sends either; **409 if batch active** (for `mode`); 400 if unknown |
| `POST` | `/api/do/upload` | operator 5000 | **Only when mode == `do_manual`** (else 409). `multipart/form-data` field `photos` (1..N); validate type/size; save under dated dir; run OCR engine; insert `draft` rows; return `{items:[{id, photo_url, no_do, plate, expected_sack, expected_box, ocr_text}]}` |
| `GET` | `/api/do/photo/<id>` | operator | Serve photo bytes (path from row; no traversal) |
| `GET` | `/api/do/staged` | operator | Rows `status IN (draft,staged)` for `get_counting_date()` |
| `PUT` | `/api/do/<id>` | operator | Edit fields; set `status=staged` when saved |
| `DELETE` | `/api/do/<id>` | operator | Remove row + photo (or mark discarded) |
| `GET` | `/api/do/settings` | both | Effective flags + **`ocr_engine`** (poll both UIs) |
| `POST` | `/api/do/settings` | **split**: `ocr_engine` → **both ports**; `require_plate`/`require_do` → **office only** (403) | Persist; 400 unknown engine value |
| `POST` | `/api/batch/start` | operator | **Mode-dependent:** `do_manual` → body `{do_ids}` + DO gates; `manual` → legacy no-DO start (existing); `auto` → **409** (FSM owns lifecycle) |
| `GET` | `/api/batch/stop-preview` | operator | `{active, zone_busy, sacks_in_zone, boxes_in_zone}` — used for **do_manual** (and optional **manual**) stop UI |
| `POST` | `/api/batch/stop` | operator | **`do_manual`/`manual` only** (auto: 409). Finalize + DO columns when present; discard only if **both** `net_sack==0` and `net_box==0` |
| `GET` | `/api/current-batch` | both | Extend with plate, do_numbers, expected_*, net so far, `mode` |
### 7.1 Start gates (`mode == do_manual` + `do.enabled`)
Reject `400` with machine-readable `reason` + Indonesian `message`:
1. Batch already active (existing).
2. Mode is not `do_manual` (e.g. `auto` / `manual`) → 409 `wrong_mode`.
3. `require_do` and zero DOs selected/staged.
4. Any selected DO missing `no_do`.
5. If `require_plate`: any selected DO missing `plate`.
6. If ≥2 DOs with **non-empty** plates and plates **disagree** → block (list plates).
7. Empty plates when `require_do` and not `require_plate`: allowed; group shows “tanpa plat”.
On success:
- Build `batch_state` as today + `"batch_mode": "do_manual"` + DO fields
(union plate if all equal, else `""`).
- Mark selected DOs `status=attached`; keep `batch_id = NULL` until stop; store
`do_ids` only in state file.
- Atomic write `current_batch.json` (existing tmp+replace).
**Legacy `manual` mode:** existing start/stop without DO gates (when
`mode == manual`). Start/stop-preview soft-warn optional same as do_manual.
**`auto` mode:** start/stop manual APIs return 409; FSM only.
### 7.2 Stop soft-warn
`stop-preview` sources (lazy OR):
- `live_status.json` / recent `current_batch` counts changing (fallback: sacks_in_zone unknown → no warn).
- Optional: dashboard reads last annotated frame only for display — **not required**.
- If `sack_count > 0` and `last_detection_time` within `do.zone_warn_seconds` (default 3s) → `zone_busy: true`.
UI: modal “Masih ada aktivitas di zona (N karung) — akhir paksa?” → second confirm → same `POST /api/batch/stop` with `force=true` (force only skips client-side block; server always allows stop).
### 7.3 Finalize SQL (stop) + discard
```sql
-- only when NOT (net_sack == 0 AND net_box == 0):
UPDATE batches SET
plate=?, do_numbers=?, expected_sack=?, expected_box=?,
net_sack=?, net_box=?,
box_loading=?, box_unloading=?, model_mode=?
WHERE counting_date=? AND batch_number=? AND camera_name=? AND object_label=?;
```
- `net_sack = count − unloading`, `net_box = box_count − box_unloading`
(values from `current_batch.json` at stop).
- **Discard path (dashboard stop, modes `do_manual`/`manual`):** if
`net_sack == 0` and `net_box == 0` → no `batches` write. Either non-zero → row.
- **`predict.py` `finalize_batch` (auto path) + dashboard stop:**
project counts **sacks and boxes** — discard only when **both** nets are 0
(`net_sack == 0 and net_box == 0`). Today `finalize_batch` discards on
`final_count == 0` alone (sack only) → **must change** so box-only batches
(auto and do_manual) are kept. Rows always can store sack count **and**
box_loading/box_unloading columns.
- Need sack `unloading` on state file: **add** it in operator-driven branch
alongside `count` (`box_unloading` already written); for auto finalize, pass
net values the same way when updating columns.
---
## 8. OCR design
Single entry: `extract_do_fields(image_path, engine) -> dict` in `src/do_ocr.py`.
Engine = effective `ocr_engine` from `do_settings.json` (fallback YAML
`do.ocr.engine` / `tesseract`) — **tesseract | paddle | none**. Upload reads
engine once per request so a UI flip applies to the next photo without restart.
Settings `ocr_engine` wins over YAML when present.
### Primary: Tesseract, CPU
- Dependency: system `tesseract-ocr` + data (`eng` minimum; `ind` if available),
Python `pytesseract` in `requirements.txt`.
- Preprocess: OpenCV grayscale → Otsu → optional deskew (minAreaRect on text mask).
- Extract with regexes (inspired by pfm-ocr patterns, simplified):
- DO number: `No.\s*DO\s*[:#]?\s*([A-Z0-9\-/]+)` and loose 10-digit fallback.
- Plate: Indonesian plate pattern + uppercase normalize.
- Table lines: token scan for unit words (sack-like / box-like table in §4)
+ adjacent integers → sums.
- Response is **draft only** — operator review is mandatory UI step.
- If `tesseract` binary missing (`engine: tesseract`) or `engine: none` → empty
draft, UI banner “OCR tidak aktif — isi manual”; upload still works.
Missing paddle deps with `engine: paddle` → explicit error, no silent fallback.
- Inference off GPU; **must not** run inside `predict.py` loop — dashboard-side
only, on upload request.
### Backup: PaddleOCR-level (`engine: paddle`)
- **Available as a first-class runtime value**, not a deferred redesign:
`ocr_engine: paddle` (settings file or YAML seed) selects the same
`extract_do_fields()` path with a PaddleOCR (PP-DocLayout /
PaddleOCR-VL-class) implementation behind it.
- Purpose: higher field accuracy when Tesseract fails acceptance
(≤30 s operator correction budget) or DO print quality is poor.
- Deploy note: heavier install (Python wheels / optional GPU); document in
`docs/deployment.md` as **optional package**, default image stays Tesseract-only.
- Swap = **UI toggle** (office monitoring **and** operator DO panel) writing
`do_settings.json` `ocr_engine` — both ports, same file, next `GET` syncs the
other page. **No process restart; no API contract change.**
- Missing paddle deps with selected engine → explicit error on upload, no
silent fallback; settings write still 200 (so UI can show engine, upload
banner explains missing package).
- Phase 2 ships the dispatch + Tesseract + both-port `ocr_engine` toggle;
Paddle backend module lands when binary/deps validated on Jetson (stub
raises clear error if engine selected but package missing).
Acceptance for keeping Tesseract: operator completes a real DO in **≤30s**
including corrections. If not → flip engine to paddle from either UI.
---
## 9. UI
### 9.0 Mode display + switch (split by port)
| Surface | Mode control |
|---|---|
| **`monitoring.html` (office 5721)** | **3-way switcher** (Otomatis / DO Manual / Manual legacy) → `POST /api/batch/mode {mode}`; handles 403/409 toasts. Primary place to enter `do_manual`. Default UI state **`auto`** (today’s `currentBatchMode = 'manual'` JS default must change). |
| **`operator.html` (5000)** | **Read-only badge** current mode. `GET` only. |
| Operator auto→manual banner | **Remove** `switchToManual()` POST (would 403). Replace with info: “Mode diatur dari monitoring/kantor” + dismiss. |
| Operator start/stop | Per mode: `auto` hidden/disabled; `manual` legacy buttons; `do_manual` DO panel + buttons. |
| Operator model A–D select | **Remove** (locked #11b) — office monitoring only. |
| OCR engine toggle | **Both** monitoring + operator: `POST /api/do/settings {ocr_engine}` allowed both ports; sync via settings poll. |
- `GET /api/batch/mode` → `{mode, model_mode, mode_editable, model_mode_editable}`
(`*_editable=true` only on office).
- **Operator model-mode selector removed** (locked #11b): no `#modelModeSelect`
/ `setModelMode()` POST from `operator.html`; show read-only active model mode
if desired (optional badge from same GET / `/api/model-modes`).
- Office monitoring owns **batch mode + model mode** switches; **both** pages own `ocr_engine` (§9.1.8).
- Switch blocked while batch active (409 → toast) for batch `mode`.
### 9.1 DO panel (only when operator sees `mode === 'do_manual'`)
1. **Ambil Foto DO (smartphone)** — `<input type="file" accept="image/*" capture="environment" multiple>`.
- Primary device: **operator’s phone** opening `http://…:5000/operator` (same LAN
or reachable host). HTML5 camera capture; multiple selects supported where the
mobile browser allows, else sequential single captures into the same strip.
- Landscape preferred for table rows; UI accepts portrait (preview crop hint only).
- Photo size: downscale server-side to max edge ~1600–2048 px before save/OCR
(phone cameras are large; keeps 7-day disk + upload time small).
- Works on mobile Safari/Chrome via the web page — **not** a Flutter/native client.
2. **Thumbnail strip** + per-DO review card:
- No. DO, Plat, Ekspektasi karung, Ekspektasi box
- Save / Delete
3. **Grup preview** — bucket by plate; chips “B 1234 · 2 DO · 80 karung · 10 box”;
warnings: mixed plates, missing plate, missing DO#.
4. **Start modal** — table of DOs to attach + expected totals; disable confirm with reason list.
5. **Active batch card** — plate, DO numbers, live:
- Karung: `net / expected_sack`
- Box: `net_box / expected_box`
6. **Stop modal** — if `stop-preview.zone_busy`: extra force button styling.
7. **Office-only `require_plate`** — show when `GET /api/do/settings` →
`{editable: true}` (office); hide when `editable: false` (403 on POST from 5000).
8. **OCR engine toggle (`ocr_engine`)** — 3-way (Tesseract / Paddle / Off):
- **Both** `operator.html` (DO panel, near capture) **and**
`monitoring.html` (office DO/settings block) get the same control.
- Write: `POST /api/do/settings {"ocr_engine": ...}` — **allowed on both
ports** (unlike `require_plate`).
- Sync: both pages poll `GET /api/do/settings` (existing settings poll or
same DO-panel interval) → flip on one device appears on the other without
reload. No websocket needed.
- Badge/label shows effective engine; upload failure for missing paddle
package shows inline error on whichever page captured the photo.
History (`history.html`): columns plate, DO list, expected, net, variance when present.
---
## 10. `predict.py` touchpoints (minimal)
| Location | Change |
|---|---|
| Mode read each frame (`batch_mode.json`) | Default missing/invalid → **`auto`** (was hard-coded `"manual"`). Accept `do_manual` as a third value; treat **any non-`auto`** as “operator-driven” for start/stop file polling (same as today’s manual branch). |
| Manual/do_manual batch active loop (`count` / `box_count`) | Also persist `unloading` (sack) next to `box_unloading` for net-at-stop. Prefer JSON fields. |
| `do_manual` DO gates | **None in predict.py** — gates live in dashboard start API only. |
| Startup load of state file | No change (opaque dict). |
| Auto mode | **No change** to FSM when `mode == auto`, except `finalize_batch` discard aligns to both-net rule (§7.3). |
| Truck detect interval / ROI | **No change** (all modes). |
If `unloading` already tracked only in memory as `counter.unloading_count`,
mirror it into `active_batch_info` wherever `count` is written (2 places in
operator-driven branch).
`predict.py` mode check becomes three-way:
```python
mode = bm_data.get("mode", "auto") # after config default_mode seed
current_batch_mode = mode if mode in ("auto", "do_manual", "manual") else "auto"
# branches: if current_batch_mode == "auto": FSM else: state-file manual/do_manual
```
---
## 11. Retention
- On dashboard start + hourly `threading.Timer`/`loop`:
- cutoff = today − `retention_days` (calendar dates under `do_photos/`).
- `DELETE FROM delivery_orders WHERE counting_date < cutoff AND status != 'attached'`
(attached rows keep photo path optional — **also purge photos** for cutoff
dates always; keep DB row for audit with `photo_path` cleared or left dangling →
prefer `UPDATE ... photo_path=''` after file delete).
- `shutil.rmtree` old date folders.
- Log summary counts at `print`/`logging`.
- Config: `do.retention_days: 7`.
Disk context (this Jetson): root `/` 27G free; photos are small JPEGs — 7 days
negligible. Prefer photos under `/opt/jetson-counter/do_photos` (same volume as DB).
---
## 12. Testing
CI stays pure-Python (no cv2/tesseract required):
| Test file | Covers |
|---|---|
| `tests/test_do_batch.py` | start gates: wrong mode, no DO, missing plate when required, mixed plates, plate optional; group-by-plate helper; net formula; discard only when both nets 0 (dashboard stop + `finalize_batch` helper) — **sack and box**; sack/box unit token classify (`dus`/`kardus` → box); retention date cutoff; mode value validation (`auto`/`do_manual`/`manual`, default auto); **mode POST 403 from operator port, 200 from office**; **model_mode POST 403 from operator, 200 from office**; **`ocr_engine` POST 200 from operator *and* office, 400 unknown value**; **`require_plate` POST still 403 from operator** (Flask test client + Host header) |
| `tests/test_config_loader.py` | extend: `do:` block defaults + overrides; `batch.default_mode` |
| Existing counting/batch tests | must stay green (no regression) |
Manual / device checks (not CI):
1. Fresh deploy / missing `batch_mode.json` → mode reads **`auto`**; FSM counts; DO panel hidden.
2. **Office** switches to `do_manual` (no active batch) → operator panel shows DO UI after poll.
3. **Operator** `POST /api/batch/mode` → **403**; banner no longer posts (info only).
4. Office switches to `manual` → legacy start/stop, no DO panel.
5. Mode switch while batch active → 409 / blocked in UI.
6. `do_manual`: stage 2 DOs same plate → start → count → stop → row has plate + do_numbers + expected.
7. Different plates → start blocked.
8. Office port toggles `require_plate`; operator port 403.
9. Stop while “zone busy” → force path.
10. Stop with both nets 0 → no batches row; boxes-only (net_box>0, net_sack=0) → row written.
11. Purge: backdate folder → restart → gone.
12. `POST /api/batch/start` in `auto` mode → 409.
13. Operator `POST` `model_mode` from port 5000 → **403**; no selector on operator page; office monitoring can change A–D.
14. **Smartphone** on site Wi‑Fi: open `:5000/operator`, capture ≥2 DO photos, review, stage, start in `do_manual`.
15. After Phase 4–5: `docs/do-erd.md` Mermaid renders; docs/CHANGELOG updated; push to origin visible in `git log`.
16. Toggle `ocr_engine` from operator `:5000` → office monitoring shows same value on next poll (and reverse); `require_plate` still 403 from operator.
Commands:
```bash
python -m pytest tests/ -q
python -m compileall predict.py counter_dashboard.py src
```
---
## 13. Deployment
- `deploy_to_jetson.py`: already syncs `counter_dashboard.py`, `operator.html`,
`config.yaml` — **also sync** `templates/monitoring.html` (office mode switcher);
no new service.
- Jetson packages (primary): `sudo apt install tesseract-ocr tesseract-ocr-ind`
(document; CI does not install it).
- Optional backup OCR: PaddleOCR stack when UI sets `ocr_engine: paddle` —
separate install notes in `docs/deployment.md`; not required for default deploy.
- Restart: `sudo systemctl restart karung-counter-dashboard` (and counter if
`predict.py` changed).
- `.gitignore`: ensure `do_photos/` under output dir only (already outside repo
when `output.dir=/opt/jetson-counter`).
---
## 14. Phases & file list
### Phase 1 — usable end-to-end (no OCR accuracy dependency)
- [x] `src/config_loader.py` — `DoConfig`; `batch.default_mode` default `"auto"`
- [x] `config.yaml` — `do:` block + `batch.default_mode: auto`
- [x] `batch_mode.json` semantics — 3 modes, default **auto**; `POST /api/batch/mode`:
office-only for `mode` **and** `model_mode`, 409-when-active, 400 unknown
- [x] `predict.py` — mode default **auto**; accept `do_manual`; persist `unloading`
in operator-driven state JSON; **`finalize_batch` discard = both nets 0**
(sack **and** box)
- [x] `counter_dashboard.py` — migrations, DO CRUD, start gates (do_manual only), stop-preview,
settings API: office-only `require_plate`/`require_do`, **both-port `ocr_engine`**;
extended start/stop/current-batch, retention job; start/stop 409 in auto;
**403 model_mode POST from operator**; photo accept/resize for **smartphone** uploads
- [x] `templates/monitoring.html` — office 3-way **batch** mode switcher + **model mode A–D** (primary);
**`ocr_engine` toggle** (syncs with operator)
- [x] `templates/operator.html` — read-only mode badge; **remove** `switchToManual` POST
**and** `#modelModeSelect` / `setModelMode()`; DO panel only in do_manual;
**smartphone capture** input + mobile-friendly review strip;
**`ocr_engine` toggle** (same settings poll as office)
- [x] `templates/history.html` + export columns
- [x] `tests/test_do_batch.py` + mode tests
- [ ] Manual check: **real phone** → `:5000/operator` → multi-photo DO flow on Wi‑Fi
### Phase 2 — OCR assist
- [x] `src/do_ocr.py` — `extract_do_fields()` + **tesseract** and **paddle**
dispatch (`none` short-circuit); paddle raises clear error if deps missing;
engine from `do_settings.json` → YAML fallback per upload request
- [x] `requirements.txt` — `pytesseract`
- [x] Wire into `/api/do/upload` (read effective `ocr_engine` each request)
- [x] UI: confirm **both** pages’ `ocr_engine` toggle wired to settings POST/GET
(control markup can land Phase 1; behavior verified here)
- [x] Unit tests with fixture image **optional** (skip if no binary)
### Phase 3 — documentation update
- [x] `docs/configuration.md` — `do:` / `batch.default_mode`, office-only mode + model_mode,
both-port `ocr_engine` runtime override
- [x] `docs/deployment.md` — tesseract packages, paddle optional, smartphone operator URL,
photo dir + 7-day retention, restart steps
- [x] `docs/architecture.md` — three batch modes; DO pipeline branch vs auto FSM
- [x] `README.md` — short DO manual flow + pointer to plan/ERD
- [x] `AGENTS.md` — mode matrix, office-only POST rules (mode/model_mode/require_*),
both-port `ocr_engine`, discard both-nets
- [x] `CHANGELOG.md` — dated entry (Keep-a-Changelog style)
- [x] `docs/do-batch-implementation-plan.md` — mark phases done / status line when shipping
### Phase 4 — entity–relationship diagram (Mermaid)
- [x] Create **`docs/do-erd.md`** (new file) with Mermaid `erDiagram` covering:
- `batches` (+ plate, do_numbers, expected_sack/box, net_sack/box, count, box_*)
- `delivery_orders` (status, photo_path, expected_*, no_do, plate)
- `daily_summaries`
- logical link: `delivery_orders` N—1 batch via `do_numbers` on stop /
state-file `do_ids` while active (document as **soft** FK — no hard FK required)
- [x] Note JSON sidecars: `current_batch.json`, `batch_mode.json`, `do_settings.json`,
photo tree `{output.dir}/do_photos/YYYY-MM-DD/`
- [x] Link ERD from `docs/architecture.md` + plan §5
### Phase 5 — commit and push
- [x] `python -m pytest tests/ -q` green; `python -m compileall` clean
- [ ] Review `git status` / `git diff` — **no** state junk (`.env`, `*.db`, `*.jpg`,
`*.engine`, `batch_mode.json`, photos) — follow `.gitignore` / AGENTS “don’t commit state”
- [ ] Stage only intended files (code, tests, `config.yaml`, templates, docs, plan, ERD)
- [ ] Commit message(s) conventional/repo style, e.g.
`feat: DO-gated manual batch mode (auto default, office-only switches)`
— split docs-only vs feat if cleaner
- [ ] `git pull --rebase` if needed; `git push` to `origin` main
(`https://git.proit.id/andrew/karung-counting-feedmill-semarang`)
- [ ] If deploy follows: `python deploy_to_jetson.py` then systemctl restart
(services listed in §13) — **only** after explicit deploy request
### Phase 6 — hardening (post-ship)
- [ ] PaddleOCR backend validated on Jetson (if Tesseract acceptance fails or
field requests it) — flip `ocr_engine: paddle` from either UI
- [ ] Soft-warn polish from field feedback
- [ ] Variance coloring on history/analytics
**Deferred / not in v1:** DB purge of old `batches` rows, multi-camera, auto
plate from ROI (impossible without ALPR), native mobile app.
---
## 15. Risks & mitigations
| Risk | Mitigation |
|---|---|
| Tesseract accuracy poor on feedmill DO | Review UI mandatory; flip `ocr_engine: paddle` from office **or** operator UI (shared `do_settings.json`); fields always editable |
| GPU contention | Tesseract path never in `predict.py`; CPU only. Paddle path also dashboard-only |
| Operator skips photos (`require_do` false) | Default `require_do: true` in production YAML (applies only in `do_manual`) |
| Mode left on `do_manual` overnight | Default seed **`auto`**; UI shows current mode badge; office flips back to auto |
| Accidental start/stop in auto | Manual APIs return 409; buttons hidden |
| Operator POSTs mode or model_mode (old UI/scripts) | **403** on operator port; remove `switchToManual()` **and** operator model selector |
| `ocr_engine` flipped wrong / paddle deps missing | Settings write still 200 (UI shows choice); upload returns clear error — no silent fallback |
| Box-only batch dropped by sack-only finalize check | Locked #1/#3: both nets required for discard; counts store sack **and** box |
| Mixed plates forced by mistake | Hard block + clear message; office can temporarily disable plate req |
| Photo disk fill | 7-day purge on `/opt/jetson-counter`; server-side downscale on phone upload |
| Phone camera too large / slow upload | Max-edge resize + JPEG quality cap before store/OCR |
| Both nets 0 drops audit | Accepted decision #1; boxes-only batches **kept**; monitor logs if frequent |
| `unloading` missing at stop | Phase 1 writes it every counter update |
---
## 16. Acceptance criteria
1. Fresh start with no `batch_mode.json` → mode is **`auto`**; FSM behaves as today.
2. **Office** can switch **auto ↔ do_manual ↔ manual** when no batch is active;
**operator POST mode → 403**; blocked while active (409).
3. In `do_manual`: operator stages ≥1 DO photo, edits fields, starts batch only when gates pass.
4. Two DOs same plate → one batch; both `do_numbers` stored on stop.
5. Two different plates → start rejected.
6. Plate required toggle and model mode A–D change only from office port;
**`ocr_engine` toggle allowed on both ports** and stays in sync on next poll.
7. Live panel shows net sack/box vs expected during batch (`do_manual`).
8. Stop with zone activity asks confirm; force completes.
9. Stop with `net_sack=0` **and** `net_box=0` → no `batches` row; either net
non-zero → row written with DO columns.
10. Photos older than 7 days removed automatically.
11. `manual` (legacy) start/stop works without DO requirements.
12. `auto` mode: manual start/stop APIs 409; DO panel hidden; counting unchanged.
13. Operator **cannot** change model mode A–D (no selector; POST 403); office can.
14. Discard rule same for dashboard stop **and** `predict.py` `finalize_batch`
(auto): keep row if **either** sack net or box net ≠ 0.
15. **Smartphone** can open operator page, capture multi DO photos, review, upload.
16. `docs/do-erd.md` exists with Mermaid ERD; docs/CHANGELOG updated (Phase 3).
17. Changes committed and pushed to origin (Phase 5) after green tests.
18. `python -m pytest tests/ -q` green in CI (no new hard deps).
19. `ocr_engine` POST **200 from operator and office**; other page shows new value on next `GET`; `require_plate` still 403 from operator.
+137
View File
@@ -0,0 +1,137 @@
# Implementation Plan — Operator Tiles + History Cleanup (DO Expected Labels)
> Status: **implemented** (UI-only; manual device checks pending).
> Scope: **UI labels/columns only** — no API/DB/predict semantics change.
> Ports (locked): **5721 = office** · **5000 = operator** (DO manual uploads, legacy manual, live counts).
> Operator does **not** need history; history/analytics stay **office-only** (`/history`, `/analytics` already gated in `counter_dashboard.py`).
---
## 1. Why
Current operator tiles in `do_manual` show **Net / Target** next to **Karung Masuk / Box Masuk**. When unloading is 0 (normal load-out), net == gross → redundant and confusing.
History has a **Net** column that duplicates sacks/boxes when net ≈ count, and **Expected** has no unit hint (sack vs box).
**Goal:** operator compares **counted** vs **DO paper qty** in four clear tiles. History drops Net; Expected is labeled as DO sacks/boxes.
---
## 2. Locked decisions
| # | Topic | Decision |
|---|---|---|
| 1 | Port split | 5721 office (mode/model switches, history, analytics, plate require) · 5000 operator (start/stop, DO photo upload, live counts). Unchanged. |
| 2 | Operator history | Operator does not consume history/count analytics; no history link/panel on `operator.html`. History page stays office-only. |
| 3 | Operator tiles | Four tiles only (see §3). **No “Net …” tiles** on operator UI. |
| 4 | DO expected naming | UI name = **DO Sacks** / **DO Boxes** (paper expected). Server fields stay `expected_sack` / `expected_box`. |
| 5 | History columns | Cleaner set (§4). **Remove Net column.** Expected labeled as DO sacks/boxes. |
| 6 | Backend NET | **Keep** `net_sack` / `net_box` in API, `current_batch.json`, DB, discard rule (`both nets 0`), exports if still needed for audit. **UI stop showing net on operator.** |
---
## 3. Operator tiles (`templates/operator.html`)
Replace current info grid rows with:
| Tile label | Value | Source (`GET /api/current-batch`) |
|---|---|---|
| **Sacks** | counted sacks | `count` (same as today’s “Karung Masuk”) |
| **Boxes** | counted boxes | `box_count` (same as “Box Masuk”) |
| **DO Sacks** | expected sacks | `expected_sack` (0 if no batch/no DO) |
| **DO Boxes** | expected boxes | `expected_box` |
Render rules:
- **All four tiles always visible** on the operator card (not only in `do_manual`).
- Non-`do_manual`: DO Sacks / DO Boxes show `0` or `--` (no staged expectation); still show label so layout is stable.
- `do_manual` + active batch: DO tiles = values from batch state (start sum of staged DOs).
- Remove rows/IDs: `opNetSackItem`, `opNetBoxItem`, and JS that writes `net_sack` / `net_box` into those tiles.
- Keep: Batch Berjalan, Waktu Mulai, Plat / No. DO (batch active), mode badge, DO panel.
DO card form labels (same vocabulary):
- `Ekspektasi karung` → **`DO Sacks`** (or `DO Sacks (karung)` if Indonesian preferred — pick one at implement; default English matches tiles).
- `Ekspektasi box` → **`DO Boxes`**.
Group preview chips unchanged (already “N karung · M box”).
Optional polish (same file, if trivial): under Sacks show `of DO Sacks` only in `do_manual` — **not** a second Net row.
---
## 4. History (`templates/history.html`) — office only
Current header:
`Batch # | Plate | No. DO | Sacks | Boxes | Expected | Net | Start | End | Duration`
**New header (no Net, clearer Expected):**
| Column | Content |
|---|---|
| Batch # | `batch_number` |
| Plate | `plate` or `—` |
| No. DO | `do_numbers` joined or `—` |
| Sacks | `count` (gross loaded; stored as `count`) |
| Boxes | `box_loading` |
| DO Sacks / Boxes | `expected_sack / expected_box` e.g. `100 / 12` |
| Start | time |
| End | time |
| Duration | minutes |
- Remove cell that prints `net_sack / net_box`.
- API: keep returning nets if already present; **UI ignores them** (or stop emitting in `day-detail` later — not required).
- colspan updates (loading, empty, error rows).
- Export day XLSX: drop Net columns if present; rename Expected headers to `DO Sacks`, `DO Boxes` (match UI).
No changes to operator gate: `GET /history` still office-only.
---
## 5. Non-goals
- No change to `net_counts` / discard-both-nets / `finalize_batch`.
- No change to ports, office-only POSTs, OCR engine toggle.
- No history page for operator.
- No API rename of `expected_*` (JSON keys stable).
---
## 6. Files
| File | Change |
|---|---|
| `templates/operator.html` | Four tiles Sacks/Boxes/DO Sacks/DO Boxes; remove Net tiles + JS |
| `templates/history.html` | Drop Net column; Expected → `DO Sacks / DO Boxes` |
| `counter_dashboard.py` | Optional: export headers only (if export shows Net/Expected) |
| `docs/do-expected-tiles-plan.md` | This plan |
| Old plan | Delete `docs/do-batch-implementation-plan.md` (superseded by shipped feature + this UI plan) |
---
## 7. Test / verify
```bash
python -m pytest tests/ -q
python -m compileall predict.py counter_dashboard.py src tests
```
Manual (office vs operator):
1. `:5000/operator` — tiles labeled Sacks, Boxes, DO Sacks, DO Boxes; no “Net …” labels.
2. `:5000/operator` in `do_manual` — start with DO → DO Sacks/Boxes = sum of DO fields; Sacks/Boxes track camera.
3. `:5721/history` — columns as §4; no Net; Expected shows `n / m`.
4. Operator browser → `/history` → still operator page (office gate).
5. Mode/model POST from `:5000` still 403 (unchanged).
---
## 8. Acceptance
1. Operator live card shows exactly: Sacks, Boxes, DO Sacks, DO Boxes (+ batch meta).
2. No operator-visible string “Net Karung”, “Net Box”, “Net / Target”.
3. History table has no Net column; Expected header communicates DO sacks/boxes.
4. History remains office-only.
5. Discard rule and DB `net_*` columns unchanged.
6. pytest + compileall green.
+5 -8
View File
@@ -265,8 +265,7 @@
<th>No. DO</th> <th>No. DO</th>
<th>Sacks</th> <th>Sacks</th>
<th>Boxes</th> <th>Boxes</th>
<th>Expected</th> <th>DO Sacks / Boxes</th>
<th>Net</th>
<th>Start Time</th> <th>Start Time</th>
<th>End Time</th> <th>End Time</th>
<th>Duration (Min)</th> <th>Duration (Min)</th>
@@ -274,7 +273,7 @@
</thead> </thead>
<tbody id="batchesTableBody"> <tbody id="batchesTableBody">
<tr> <tr>
<td colspan="10" class="no-data"> <td colspan="9" class="no-data">
<i class="fa-solid fa-arrow-left"></i> <i class="fa-solid fa-arrow-left"></i>
Silakan pilih tanggal dari daftar sebelah kiri untuk memuat detail batch. Silakan pilih tanggal dari daftar sebelah kiri untuk memuat detail batch.
</td> </td>
@@ -358,7 +357,7 @@
const tableBody = document.getElementById('batchesTableBody'); const tableBody = document.getElementById('batchesTableBody');
tableBody.innerHTML = ` tableBody.innerHTML = `
<tr> <tr>
<td colspan="10" class="no-data"> <td colspan="9" class="no-data">
<i class="fa-solid fa-circle-notch fa-spin"></i> <i class="fa-solid fa-circle-notch fa-spin"></i>
Memuat data batch untuk tanggal ${date}... Memuat data batch untuk tanggal ${date}...
</td> </td>
@@ -384,7 +383,7 @@
if (!data.batches || data.batches.length === 0) { if (!data.batches || data.batches.length === 0) {
tableBody.innerHTML = ` tableBody.innerHTML = `
<tr> <tr>
<td colspan="10" class="no-data"> <td colspan="9" class="no-data">
<i class="fa-solid fa-inbox"></i> <i class="fa-solid fa-inbox"></i>
Tidak ada batch tercatat pada tanggal ${date}. Tidak ada batch tercatat pada tanggal ${date}.
</td> </td>
@@ -401,7 +400,6 @@
const dos = (b.do_numbers || []).join(', ') || '—'; const dos = (b.do_numbers || []).join(', ') || '—';
const plate = b.plate || '—'; const plate = b.plate || '—';
const expected = (b.expected_sack || 0) + ' / ' + (b.expected_box || 0); const expected = (b.expected_sack || 0) + ' / ' + (b.expected_box || 0);
const net = (b.net_sack ?? b.count) + ' / ' + (b.net_box ?? b.box_loading ?? 0);
tr.innerHTML = ` tr.innerHTML = `
<td style="font-weight: 600; color: var(--accent-blue);">Batch #${b.batch_number}</td> <td style="font-weight: 600; color: var(--accent-blue);">Batch #${b.batch_number}</td>
@@ -410,7 +408,6 @@
<td style="font-weight: 500;">${b.count.toLocaleString()}</td> <td style="font-weight: 500;">${b.count.toLocaleString()}</td>
<td style="font-weight: 500;">${(b.box_loading || 0).toLocaleString()}</td> <td style="font-weight: 500;">${(b.box_loading || 0).toLocaleString()}</td>
<td style="font-size:12px;">${expected}</td> <td style="font-size:12px;">${expected}</td>
<td style="font-size:12px;">${net}</td>
<td>${startStr}</td> <td>${startStr}</td>
<td>${endStr}</td> <td>${endStr}</td>
<td>${b.duration_minutes || 0} min</td> <td>${b.duration_minutes || 0} min</td>
@@ -422,7 +419,7 @@
console.error('Failed to load date details:', err); console.error('Failed to load date details:', err);
tableBody.innerHTML = ` tableBody.innerHTML = `
<tr> <tr>
<td colspan="10" class="no-data" style="color: var(--accent-red);"> <td colspan="9" class="no-data" style="color: var(--accent-red);">
<i class="fa-solid fa-circle-exclamation"></i> <i class="fa-solid fa-circle-exclamation"></i>
Gagal memuat detail data: ${err.message} Gagal memuat detail data: ${err.message}
</td> </td>
+18 -21
View File
@@ -366,20 +366,20 @@
<div class="info-val" id="opStartTime">--:--:--</div> <div class="info-val" id="opStartTime">--:--:--</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="info-label">Karung Masuk</div> <div class="info-label">Sacks</div>
<div class="info-val" id="opSackCount">0</div> <div class="info-val" id="opSackCount">0</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="info-label">Box Masuk</div> <div class="info-label">Boxes</div>
<div class="info-val" id="opBoxCount">0</div> <div class="info-val" id="opBoxCount">0</div>
</div> </div>
<div class="info-item" id="opNetSackItem" style="display:none"> <div class="info-item">
<div class="info-label">Net Karung / Target</div> <div class="info-label">DO Sacks</div>
<div class="info-val" id="opNetSack">--</div> <div class="info-val" id="opDoSack">0</div>
</div> </div>
<div class="info-item" id="opNetBoxItem" style="display:none"> <div class="info-item">
<div class="info-label">Net Box / Target</div> <div class="info-label">DO Boxes</div>
<div class="info-val" id="opNetBox">--</div> <div class="info-val" id="opDoBox">0</div>
</div> </div>
<div class="info-item" id="opPlateItem" style="display:none; grid-column: 1 / -1;"> <div class="info-item" id="opPlateItem" style="display:none; grid-column: 1 / -1;">
<div class="info-label">Plat / No. DO</div> <div class="info-label">Plat / No. DO</div>
@@ -646,9 +646,9 @@
<input data-id="${d.id}" data-k="no_do" value="${escapeHtml(d.no_do || '')}"></div> <input data-id="${d.id}" data-k="no_do" value="${escapeHtml(d.no_do || '')}"></div>
<div><label>Plat</label> <div><label>Plat</label>
<input data-id="${d.id}" data-k="plate" value="${escapeHtml(d.plate || '')}"></div> <input data-id="${d.id}" data-k="plate" value="${escapeHtml(d.plate || '')}"></div>
<div><label>Ekspektasi karung</label> <div><label>DO Sacks</label>
<input data-id="${d.id}" data-k="expected_sack" type="number" min="0" value="${d.expected_sack || 0}"></div> <input data-id="${d.id}" data-k="expected_sack" type="number" min="0" value="${d.expected_sack || 0}"></div>
<div><label>Ekspektasi box</label> <div><label>DO Boxes</label>
<input data-id="${d.id}" data-k="expected_box" type="number" min="0" value="${d.expected_box || 0}"></div> <input data-id="${d.id}" data-k="expected_box" type="number" min="0" value="${d.expected_box || 0}"></div>
<div class="row-actions"> <div class="row-actions">
<button class="btn btn-primary btn-sm" data-act="save" data-id="${d.id}">Simpan</button> <button class="btn btn-primary btn-sm" data-act="save" data-id="${d.id}">Simpan</button>
@@ -856,19 +856,16 @@
sackCount.textContent = data.count || 0; sackCount.textContent = data.count || 0;
boxCount.textContent = data.box_count || 0; boxCount.textContent = data.box_count || 0;
const netSack = document.getElementById('opNetSack'); const doSack = document.getElementById('opDoSack');
const netBox = document.getElementById('opNetBox'); const doBox = document.getElementById('opDoBox');
const netSackItem = document.getElementById('opNetSackItem');
const netBoxItem = document.getElementById('opNetBoxItem');
const plateItem = document.getElementById('opPlateItem'); const plateItem = document.getElementById('opPlateItem');
const plateDos = document.getElementById('opPlateDos'); const plateDos = document.getElementById('opPlateDos');
const showNet = currentMode === 'do_manual' && data.success && data.batch_number; const batchActive = !!(data.success && data.batch_number);
if (netSackItem) netSackItem.style.display = showNet ? 'block' : 'none'; const showPlate = currentMode === 'do_manual' && batchActive;
if (netBoxItem) netBoxItem.style.display = showNet ? 'block' : 'none'; if (doSack) doSack.textContent = data.expected_sack || 0;
if (plateItem) plateItem.style.display = showNet ? 'block' : 'none'; if (doBox) doBox.textContent = data.expected_box || 0;
if (showNet) { if (plateItem) plateItem.style.display = showPlate ? 'block' : 'none';
netSack.textContent = `${data.net_sack ?? data.count} / ${data.expected_sack ?? '-'}`; if (showPlate) {
netBox.textContent = `${data.net_box ?? data.box_count} / ${data.expected_box ?? '-'}`;
plateDos.textContent = (data.plate || 'tanpa plat') + plateDos.textContent = (data.plate || 'tanpa plat') +
(data.do_numbers && data.do_numbers.length ? ' · ' + data.do_numbers.join(', ') : ''); (data.do_numbers && data.do_numbers.length ? ' · ' + data.do_numbers.join(', ') : '');
} }