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>
This commit is contained in:
1 parent
2febe0c886
commit
ada6488592
67 files changed
+5705
-1142
No files matched your search
@@ -52,6 +52,97 @@ workflow and have no task numbers; see `git log` for real dates/history.
|
||||
|
||||
*(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.
|
||||
@@ -82,3 +173,49 @@ workflow and have no task numbers; see `git log` for real dates/history.
|
||||
- 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.
|
||||
Reference in new issue
Block a user