# Feature List (Flutter) Structured log of shipped features, updated by the `n`/`next` workflow (see [AGENTS.md](../AGENTS.md)) whenever a task is marked `[DONE]`. Organize entries under a heading per module/section, matching `plans/next-enhancements.md`. Scoped to **Flutter only** as of 2026-07-08 (see `AGENTS.md`'s "Scope: excludes `backend/`") — the Backend sections below are a frozen historical record; the active backend feature log now lives at [backend/docs/feature-list.md](../backend/docs/feature-list.md), driven by `backend/AGENTS.md` Part B. ## Format ``` ##
- **** — shipped ``` --- ## Existing Features (pre-kit) Backfilled 2026-07-08 during adoption of this kit — these predate the `e`/`n` workflow and have no task numbers; see `git log` for real dates/history. ### Flutter — Auth & Splash - Login screen with client-side validation; JWT session persisted via Hive; splash auto-navigates based on session state. ### Flutter — Camera Capture & Geotagging - Laplacian-variance blur detection with a pass/fail badge before upload. - IMU stillness lock (linear/angular thresholds) gating the shutter. - GPS geotagging on both camera capture and gallery picks. ### Flutter — Pending Documents Queue - In-memory upload queue with per-item status (uploading/processing/success/error), retry (preserving original GPS), delete, and search filter. ### Flutter — Document Editor & PDF Receipt - Header field validation (date, PO/SO number formats), item CRUD validated against the master SKU registry, signature + confirmation submission, local PDF receipt printing. ### Backend — Next.js API Gateway - Upload/parse/documents CRUD routes, GPU status endpoint, vLLM proxy, manual-label review tool. ### Backend — OCR Pipeline & Accuracy - PaddleOCR + vLLM classification pipeline with DB layout caching, table column-shift correction, date normalization, and an accuracy regression harness (`backend/pfm-web-app/scripts/accuracy-check.mts`) currently at ~89.4% overall (target 95% — see `backend/CLAUDE.md`). ### Backend — Postgres Data Layer - Schema/init in `backend/pfm-web-app/src/db/init.ts`, served via the canonical root `docker-compose.yml` stack. ### DevOps — Docker & Dev Tunnel - Root `docker-compose.yml` (dev, hot-reload) and `docker-compose.demo.yml` (production-mode override); `start-dev-tunnel.ps1` syncs the host LAN IP into the Flutter app config and starts an ngrok tunnel. *(New features shipped via `n`/`next` go below, organized the same way, with task numbers.)* ## Flutter — Auth & Splash - **1.3** Added a confirmation dialog before logout when the pending documents queue has unsynced items (`CameraDrawer._handleLogout()`, `lib/features/camera/camera_drawer.dart`). Previously, tapping Logout in the drawer called `AuthNotifier.logout()` unconditionally, which clears the local Hive cache and the in-memory pending queue (`localStorage.clearAll()` + `pendingDocumentsProvider.notifier.clearQueue()`) — any document still uploading, awaiting review, or stuck in `syncFailed` was silently destroyed. Now, if the queue is non-empty, a dialog ("Ada Dokumen Belum Tersinkron", with the pending count) requires an explicit "Ya, Logout" confirmation before proceeding; an empty queue still logs out immediately with no extra tap. Covered by `test/camera_drawer_logout_test.dart` (3 cases: no pending items, cancel with pending items, confirm with pending items) — shipped 2026-07-10. ## Flutter — Product Scan Review Flow - **7.2** Eliminated the duplicate GPU classification pass (gap G3), sourced from user feedback ("kenapa harus dilakukan dua kali... GPU tidak 2x kerja") after they noticed Product Scan's review screen took visibly longer to open than DO Scan's. Root cause: the backend already classified the photo once at upload time, but the editor's `_fetchClassificationAndSkus()` re-uploaded the same image and re-ran the *entire* classify+OCR pipeline a second time via `POST /scan-product`, purely to get the top-5 candidates and OCR-extracted expiry date the first pass could have produced too. Fixed on the backend (see `backend/docs/feature-list.md`'s 11.1 entry) by having the upload-time parse persist that full result; the editor now reads `DocumentModel.productScanMatches`/`productScanExtractedExpiryDate` directly from the already-loaded document — synchronous, no network call — mirroring exactly how `EditorScreen._loadDocumentData()` reads a DO document's data with no request at all. Keeps one lightweight fallback (`GET /master/skus`, a plain DB read with no GPU involved) for the rare case where a document has zero stored matches. `_loading` no longer starts `true`, so there's no spinner on the happy path. Tests: `test/document_product_scan_field_test.dart` (3 cases for the new model fields) and `test/product_editor_no_double_classify_test.dart` (proves a document with stored matches renders immediately with no network call). Verified live against the real backend: a genuinely fresh upload took 9s (one real GPU pass) and the immediate `GET /documents/:id` already contained 5 real candidate matches and the extracted expiry date, before any editor interaction. Full suite 63/63 pass, `flutter analyze` clean — shipped 2026-07-10. - **Ad-hoc (Simplify Product Scan form)** — Shipped 2026-07-10. Removed the "Hubungkan ke PO Dokumen" field from `ProductDropdownCard` per user request — the Product Scan review form is now exactly 3 fields (SKU, Pilih Batch, Catatan). Below the SKU dropdown, the matched product's name is now shown as plain informational text (not a form field), mirroring `ProductExpiryCard`'s existing "Tanggal terdeteksi otomatis dari OCR" info row below the batch dropdown. Removed the now-unused `_selectedRelatedPo`/`_doDocs`/`_loadDoDocs()` state from `product_editor_data_logic.dart` (was Hive-reading local DO documents to populate the removed dropdown); `product_editor_submit_logic.dart` no longer sets `noPo` from a manual PO link (now always `''` for Product Scan documents — no PO-link concept remains for this doc type). `DocumentCard`'s Product Scan subtitle no longer appends `• {noPo}` (now just shows the expiry date). Covered by `test/product_dropdown_card_test.dart` (updated + 1 new case verifying the PO field is gone and the product name info row is shown). - **7.3 (G7 half)** Replaced silently-fabricated data in the Product Scan review flow (`ProductEditorScreen`) with honest fallback states. Previously: (1) any total classification-fetch failure populated three hardcoded fake SKU matches with fake confidence scores, indistinguishable from a real AI result; (2) whenever OCR found no expiry date, two fabricated future dates were offered as if they were real batches; (3) `ProductExpiryCard` displayed a literal hardcoded "92.4%" OCR Confidence Score unconditionally — there is no real confidence signal for expiry OCR anywhere in the pipeline, so this number was always fake. Now: a genuinely unrecoverable failure (the SKU master list itself can't load) shows an explicit "Gagal Memuat Klasifikasi Produk" retry screen; a classification-only failure degrades gracefully to manual SKU selection from the real master list, with an honest "Tidak ada rekomendasi otomatis" notice instead of a fake confidence bar; a missing expiry date forces manual entry instead of offering fake batches; and the expiry card now shows an honest "terdeteksi otomatis" / "diinput manual" source label instead of a fabricated percentage. Covered by `test/product_dropdown_card_test.dart`, `test/product_expiry_card_test.dart`, `test/product_editor_classification_failure_test.dart` — shipped 2026-07-10. - **7.3 (G4 half)** Replaced the `orderUntuk == 'PRODUCT SCAN'` magic-string document-type check with a real `docType` field on `DocumentModel`, sourced from the backend's persisted `scan_mode` column (backend task 9.1, `document-mapper.ts`'s `docType`) once that shipped. Previously, editing a document's `orderUntuk` display text (a corrected/OCR'd field) could silently move it between the "DO Scan" and "Product Scan" tabs; now the tab/layout/receipt-format decision reads a dedicated field that isn't user-editable display text. Falls back to the legacy `orderUntuk` sentinel only for responses/cached data that predate the backend column. Updated call sites: `document_card.dart`, `documents_screen.dart`, `pdf_service.dart`, `product_editor_logic.dart`. Covered by `test/document_doctype_test.dart`, including a regression test reproducing the exact old bug trigger — shipped 2026-07-10. - **7.1** Moved the product editor's classification/SKU-lookup calls off the classic, unauthenticated dev routes (`GET /api/skus`, `POST /api/scan-pfm`, reached via a `apiBaseUrl.replaceAll('/api/v1', ...)` string hack) onto the authenticated v1 surface (`GET /api/v1/master/skus`, `POST /api/v1/scan-product`) once backend tasks 9.2/9.3 shipped it — fixes product scanning being broken off-LAN (backend task 4.5 restricts the public tunnel to `/api/v1/*` only). New endpoint constants in `app_config.dart`; the auth token is attached automatically by `ApiClient`'s existing interceptor, same as every other v1 call. Also switched the classification request from a base64 JSON body to multipart (`FormData`/`MultipartFile`, reusing the pattern already proven for DO uploads) to avoid a multi-MB JSON payload, matching what backend 9.3 was built to prefer. Extracted the v1-envelope-unwrapping logic into a new pure file, `lib/features/editor/product_scan_response_parser.dart`, mirroring task 6.2's testable-pure-function pattern. Tests: `test/product_scan_response_parser_test.dart` (6 cases). Verified live against the real backend: a real store account succeeds against both new endpoints with the expected response shapes — shipped 2026-07-10. ## Flutter — API Contract & Sync Integrity (DO flow) - **6.1** Replaced whole-list polling with per-document polling for pending uploads. Previously, `_pollUntilParsed` fetched the *entire* `GET /documents` list every 2 seconds and inferred "parse done" from the pending item's document appearing in a `parsed=true`-filtered list via an object-identity check — an `ocr_items` query per document per poll on the server, growing with history, and no way to distinguish "still parsing" from "parse failed" (a server-side parse failure only ever surfaced as a generic 260-second timeout). Now polls `GET /api/v1/documents/:id` for just that item's own id (backend task 9.1), reading a real `parseStatus` (`pending`/`done`/`failed`) instead of guessing from list membership — a failed parse now ends the poll immediately with an explicit error message instead of waiting out the full timeout. The done/failed/pending decision lives in a new pure function, `resolvePollOutcome()` (`lib/features/documents/poll_outcome.dart`), unit tested without mocking Dio. Verified live against the running backend stack — a real document's `GET /api/v1/documents/:id` response matches the shape the new code expects. Covered by `test/poll_outcome_test.dart` (4 cases) — shipped 2026-07-10. - **6.2** Fixed a data-integrity bug where `DocumentsScreen`'s server refresh would silently overwrite a driver's corrected-but-unsynced document with the server's stale pre-edit copy (or drop it from the visible list) whenever its `PUT /documents/:id` save had failed and it sat in the pending queue as `syncFailed`. Added a pure, unit-tested merge function (`mergeDocumentsWithUnsyncedOverrides()`, `lib/features/documents/document_sync_merge.dart`) that lets a matching `syncFailed` pending item's corrected copy override the server's version (and survive even if the server list omits that document entirely). `DocumentCard` now shows a "Belum Tersinkron" badge (instead of a hardcoded "Terkonfirmasi") for any document currently overridden this way, so the driver can see at a glance which history entries still need a retry from the pending queue. Covered by `test/document_sync_merge_test.dart` and `test/document_card_unsynced_badge_test.dart` — shipped 2026-07-10. - **6.3** Fixed a data-integrity bug where saving in either editor could PUT to a fabricated client-side ID (a millisecond timestamp) instead of the real server-assigned document id, whenever the editor opened without a resolved server document (e.g. after a process-death restart lost the `pendingId` route param) — the PUT would target a nonexistent id and the item would loop in `syncFailed` forever with no way to recover. Now, when no server document is resolved but the pending item's local image is still available, the save flow auto-recovers by re-uploading that image (safe: the backend dedups by `file_hash`) to get a real id, then PUTs the corrected fields to it; only when neither a server document nor a local image is available does it block with an explicit error instead of guessing an id. The decision logic lives in a new pure function, `resolveDocumentSaveAction()` (`lib/features/editor/document_save_action.dart`), unit tested without mocking Dio. Verified live against the running backend: re-uploaded a real test image to get a genuine server id, PUT corrected fields to it, then confirmed via GET that the correction persisted. Covered by `test/document_save_action_test.dart` (3 cases) — shipped 2026-07-10. ## Flutter — Pending Documents Queue - **3.1** Persisted the pending documents queue to a new Hive box (`lib/core/storage/local_storage.dart`) instead of holding it in memory only — an OS-level app kill mid-upload no longer silently loses the document. On next launch, `PendingDocumentsNotifier` reloads the queue and resumes anything left non-terminal: re-uploads a fresh capture from scratch (safe due to server-side file-hash dedup) or resumes polling for one whose upload already completed. Storage failures degrade gracefully to the old in-memory-only behavior rather than crashing the app — shipped 2026-07-08. ## Backend — Postgres Data Layer - **7.1** Wrapped the `ocr_items` delete-then-reinsert in `/api/parse` and `/api/v1/documents/[id]` PUT inside a DB transaction (`withTransaction` helper, `backend/pfm-web-app/src/db/index.ts`) — a mid-loop insert failure now rolls back to the previous item set instead of leaving a document with a correct header but partial/missing items — shipped 2026-07-08. ## Flutter — Document Editor & PDF Receipt - **Ad-hoc (Product Scan flow)** — Shipped 2026-07-09 - Implemented mock upload and response flow for Product Scanner review. Created a dedicated `ProductEditorScreen` with crop preview, AI classification confidence indicator, and dynamic drop-down SKU selector (Fiesta Spicy Chicken Nugget, Akumo Coin, Fiesta Schnitzel). - **Ad-hoc (Fuzzy Batch Expiry Selector)** — Shipped 2026-07-09 - Built dynamic expiry date batch matching dropdown (Batch 1, Batch 2, manual input date picker) based on selected Master SKU product. - **Ad-hoc (Custom Product PDF)** — Shipped 2026-07-09 - Redesigned the printed PDF receipt layout specifically for Product Scan (titles, headers, detail table parameters, store staff signatures) while preserving the same consistent professional layout styling as DO Scan receipt. - **Ad-hoc (PO Document Relationship)** — Shipped 2026-07-09 - Added "Hubungkan ke PO Dokumen" dropdown to select and relate the Product Scan to a confirmed DO Scan document via PO Number (`noPo` relational field). ## Flutter — Pending Documents Queue / History - **Ad-hoc (Custom Tab Switcher)** — Shipped 2026-07-09 - Switched the documents history view to show two tabs (DO Scan and Product Scan) with DO Scan as default, removing the "All" tab. - **Ad-hoc (Document Card Alignment)** — Shipped 2026-07-09 - Styled DO Scan and Product Scan history cards with matched structures: - Top category label: Staff Name for DO Scan (Orange), Product Name for Product Scan (Green). - Main Title: PO Number for DO Scan (directly without prefix), Batch Number for Product Scan (`[Batch Name]`). - Subtitle Details: `[Date] • [Item Count] Item` for DO cards, and `[Expiry Date] • [PO Number]` (without prefix labels) for Product cards. - **Ad-hoc (Active Account Seeding)** — Shipped 2026-07-09 - Seeded 1 initial mock DO Scan document and 1 Product Scan document matching the currently logged-in account/store code if empty. - **Ad-hoc (DO & Product Scan Workflow Realignment)** — Shipped 2026-07-09 - Synced default history view tab (`_selectedTab`) with the active scanner mode (`scanModeProvider`) on page load. - Realigned category identifiers (`orderUntuk`) of newly captured product scan documents to `'PRODUCT SCAN'` so they appear under the correct tab immediately instead of switching tabs after edit/confirmation. - Filtered the pending document queue display by tab category so pending DO documents and pending Product documents are kept separated. - Implemented secure store-level data isolation by clearing the local Hive cache boxes on user logout, and delegating document list filtering directly to the backend API (`GET /api/v1/documents`) which filters Postgres results by the active account's `kode_toko`. This fixes the issue where DO documents were hidden due to string mismatches in client-side kepadaYth checks. - Dynamic store profiling for newly captured mock product documents using `SharedPreferences`. - Modularized `DocumentsScreen` (bringing it under 256 lines) by extracting widgets into helper components: `DocumentsTabSwitcher`, `DocumentsEmptyState`, and `DocumentsMockSeeder`. ## Flutter — Scan Mode UX & Confirmation Gate (§8) - **8.1** Global scan-mode state + DO/Product color cue — shipped 2026-07-10 - `scanModeProvider` is now the **single source of truth** for the active tab. `DocumentsScreen._selectedTab` removed; `build()` reads `ref.watch(scanModeProvider)`; `DocumentsTabSwitcher.onTabChanged` writes `ref.read(scanModeProvider.notifier).state = tab`. Eliminates the desync where changing mode in the camera drawer was not reflected in the documents list tab (and vice-versa). - `AppConfig.doModeColor` (`Color(0xFFF57C00)`) added as a shared constant — formalizes the previously ad-hoc `Colors.orange.shade700` used only in `document_card.dart`. - Color cue applied consistently across three call sites: - `DocumentsTabSwitcher._buildTabItem`: active DO tab text → `doModeColor` (was `primaryColor`). - `CameraDrawerModeToggle._buildSegment`: active DO segment → `doModeColor` (was `primaryColor`). - `DocumentCard`: category label for DO → `doModeColor` (was inline `Colors.orange.shade700`). - **Follow-up (same day)**: user clarified the icon, not just the text, should carry the mode color — `DocumentsTabSwitcher` gained `Icons.description`/ `Icons.inventory_2` (matching `CameraDrawerModeToggle`'s existing vocabulary), colored the same as the tab's text (`doModeColor`/`primaryColor` active, `textSecondary` inactive). Deliberately scoped to the mode-toggle controls only — generic/default icons elsewhere (search, print, back, tooltips) were left untouched per explicit instruction. - `CameraDrawer` split into three files to satisfy AGENTS.md §3 (256-line threshold on touched files): `camera_drawer.dart` (232 lines, structure + logout), `camera_drawer_mode_toggle.dart` (125 lines, DO/Product pill), `camera_drawer_helpers.dart` (78 lines, section header, drawer item, dialogs). - **8.2** Added an optional `confirmed` field to `DocumentModel` (default `true`, same fallback pattern as `docType`/`parseStatus`) once backend task 10.1 shipped `confirmed` on `GET /api/v1/documents`/`:id` — the client half of closing gap G11 (documents no longer showing in history before the user taps "Simpan & Konfirmasi"; see backend `docs/feature-list.md`'s 10.1/10.2 entries for the server-side fix). No other Flutter changes were needed: the existing `mergeDocumentsWithUnsyncedOverrides()` (task 6.2) and the pending queue's "Tertunda & Diproses" section already handled the new server behavior correctly without modification — verified by re-reading both, not assumed. Covered by `test/document_confirmed_field_test.dart` (3 cases) — shipped 2026-07-10. - Tests: 7 new widget tests in `test/scan_mode_color_test.dart` covering all three color-cue assertions and provider write-through. All 50 suite tests pass.