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>
222 lines
20 KiB
Markdown
222 lines
20 KiB
Markdown
# 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
|
|
|
|
```
|
|
## <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.
|