Files
pfm-ocr/docs/feature-list.md
T
Rafhan Mazaya FathurrahmanandClaude Sonnet 5 ada6488592 feat(app): scan-mode sync, confirmation-gated documents, single-pass product classification
Fixes reported from APK field testing: DO/Product scan mode was inconsistent
between the camera drawer and documents screen (now one shared provider,
with an orange/green color cue); unconfirmed scans leaked into history with
placeholder data before the user tapped confirm (backend now gates
GET /documents on a new `confirmed` column, flipped only by PUT); and
Product Scan ran the GPU classifier twice, once at upload and again on
review (now a single pass at upload, persisted and read directly by the
editor). Also removes the unused "Hubungkan ke PO" field and fabricated
PO/SO/DO placeholder values from the Product Scan flow, closes out the
per-document-polling and save-recovery tasks (6.1/6.3), and splits several
touched files to stay under the repo's 256-line guideline.

Full detail in docs/iteration-log.md and backend/docs/iteration-log.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 15:19:32 +07:00

20 KiB

Feature List (Flutter)

Structured log of shipped features, updated by the n/next workflow (see 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, driven by backend/AGENTS.md Part B.

Format

## <Section / Module Name>

- **<task number>** <feature description> — shipped <date>

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.