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>
20 KiB
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% — seebackend/CLAUDE.md).
Backend — Postgres Data Layer
- Schema/init in
backend/pfm-web-app/src/db/init.ts, served via the canonical rootdocker-compose.ymlstack.
DevOps — Docker & Dev Tunnel
- Root
docker-compose.yml(dev, hot-reload) anddocker-compose.demo.yml(production-mode override);start-dev-tunnel.ps1syncs 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 calledAuthNotifier.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 insyncFailedwas 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 bytest/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 viaPOST /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 (seebackend/docs/feature-list.md's 11.1 entry) by having the upload-time parse persist that full result; the editor now readsDocumentModel.productScanMatches/productScanExtractedExpiryDatedirectly from the already-loaded document — synchronous, no network call — mirroring exactly howEditorScreen._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._loadingno longer startstrue, so there's no spinner on the happy path. Tests:test/document_product_scan_field_test.dart(3 cases for the new model fields) andtest/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 immediateGET /documents/:idalready contained 5 real candidate matches and the extracted expiry date, before any editor interaction. Full suite 63/63 pass,flutter analyzeclean — shipped 2026-07-10. - Ad-hoc (Simplify Product Scan form) — Shipped 2026-07-10. Removed the
"Hubungkan ke PO Dokumen" field from
ProductDropdownCardper 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), mirroringProductExpiryCard's existing "Tanggal terdeteksi otomatis dari OCR" info row below the batch dropdown. Removed the now-unused_selectedRelatedPo/_doDocs/_loadDoDocs()state fromproduct_editor_data_logic.dart(was Hive-reading local DO documents to populate the removed dropdown);product_editor_submit_logic.dartno longer setsnoPofrom 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 bytest/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)ProductExpiryCarddisplayed 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 bytest/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 realdocTypefield onDocumentModel, sourced from the backend's persistedscan_modecolumn (backend task 9.1,document-mapper.ts'sdocType) once that shipped. Previously, editing a document'sorderUntukdisplay 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 legacyorderUntuksentinel 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 bytest/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 aapiBaseUrl.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 inapp_config.dart; the auth token is attached automatically byApiClient'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,
_pollUntilParsedfetched the entireGET /documentslist every 2 seconds and inferred "parse done" from the pending item's document appearing in aparsed=true-filtered list via an object-identity check — anocr_itemsquery 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 pollsGET /api/v1/documents/:idfor just that item's own id (backend task 9.1), reading a realparseStatus(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'sGET /api/v1/documents/:idresponse matches the shape the new code expects. Covered bytest/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 itsPUT /documents/:idsave had failed and it sat in the pending queue assyncFailed. Added a pure, unit-tested merge function (mergeDocumentsWithUnsyncedOverrides(),lib/features/documents/document_sync_merge.dart) that lets a matchingsyncFailedpending item's corrected copy override the server's version (and survive even if the server list omits that document entirely).DocumentCardnow 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 bytest/document_sync_merge_test.dartandtest/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
pendingIdroute param) — the PUT would target a nonexistent id and the item would loop insyncFailedforever 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 byfile_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 bytest/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,PendingDocumentsNotifierreloads 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_itemsdelete-then-reinsert in/api/parseand/api/v1/documents/[id]PUT inside a DB transaction (withTransactionhelper,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
ProductEditorScreenwith crop preview, AI classification confidence indicator, and dynamic drop-down SKU selector (Fiesta Spicy Chicken Nugget, Akumo Coin, Fiesta Schnitzel).
- Implemented mock upload and response flow for Product Scanner review. Created a dedicated
- 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 (
noPorelational field).
- Added "Hubungkan ke PO Dokumen" dropdown to select and relate the Product Scan to a confirmed DO Scan document via PO Number (
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] Itemfor DO cards, and[Expiry Date] • [PO Number](without prefix labels) for Product cards.
- Styled DO Scan and Product Scan history cards with matched structures:
- 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'skode_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, andDocumentsMockSeeder.
- Synced default history view tab (
Flutter — Scan Mode UX & Confirmation Gate (§8)
- 8.1 Global scan-mode state + DO/Product color cue — shipped 2026-07-10
scanModeProvideris now the single source of truth for the active tab.DocumentsScreen._selectedTabremoved;build()readsref.watch(scanModeProvider);DocumentsTabSwitcher.onTabChangedwritesref.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-hocColors.orange.shade700used only indocument_card.dart.- Color cue applied consistently across three call sites:
DocumentsTabSwitcher._buildTabItem: active DO tab text →doModeColor(wasprimaryColor).CameraDrawerModeToggle._buildSegment: active DO segment →doModeColor(wasprimaryColor).DocumentCard: category label for DO →doModeColor(was inlineColors.orange.shade700).
- Follow-up (same day): user clarified the icon, not just the text, should
carry the mode color —
DocumentsTabSwitchergainedIcons.description/Icons.inventory_2(matchingCameraDrawerModeToggle's existing vocabulary), colored the same as the tab's text (doModeColor/primaryColoractive,textSecondaryinactive). Deliberately scoped to the mode-toggle controls only — generic/default icons elsewhere (search, print, back, tooltips) were left untouched per explicit instruction. CameraDrawersplit 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
confirmedfield toDocumentModel(defaulttrue, same fallback pattern asdocType/parseStatus) once backend task 10.1 shippedconfirmedonGET /api/v1/documents/:id— the client half of closing gap G11 (documents no longer showing in history before the user taps "Simpan & Konfirmasi"; see backenddocs/feature-list.md's 10.1/10.2 entries for the server-side fix). No other Flutter changes were needed: the existingmergeDocumentsWithUnsyncedOverrides()(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 bytest/document_confirmed_field_test.dart(3 cases) — shipped 2026-07-10.- Tests: 7 new widget tests in
test/scan_mode_color_test.dartcovering all three color-cue assertions and provider write-through. All 50 suite tests pass.
- Tests: 7 new widget tests in