docs(plan): enhance-refresh Flutter backlog (27 grounded tasks) + Stocks feature plan

Adds tasks 1.4-8.5 across existing sections 1-8 (each traced to a specific
file/line, not invented busywork) plus a new section 9 (Stocks Menu &
DO-to-Stock flow) seeded from a user-directed, grilled ad-hoc feature
request - full design doc in docs/stock-feature-plan.md, status: planned,
no code yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xsxk4ZkDQVVaLUcixDcqb5
This commit is contained in:
Rafhan Mazaya FathurrahmanandClaude Sonnet 5 committed 2026-07-14 08:32:34 +07:00
1 parent e23cf7c73d
commit dc0dd81318
2 files changed
+400 -4

No files matched your search

+305
View File
@@ -0,0 +1,305 @@
# Stocks Feature — Full-Stack Implementation Plan
Written 2026-07-10 after an extensive grilling/clarification session with the user
(see chat history — not reproduced here). This is the context doc for the Stocks
backlog entries in root [`plans/next-enhancements.md`](../plans/next-enhancements.md)
§9 and [`backend/plans/next-enhancements.md`](../backend/plans/next-enhancements.md)
§12 — read it before picking up any task from either section, same relationship
[`docs/api-contract-map.md`](api-contract-map.md) has to its own gap-fix tasks.
**Status: planned, not yet implemented.** Nothing described below exists in the
codebase yet — this doc is the design record to build from when the tasks below are
picked up via `n`/`next`.
## Context
The app currently tracks Delivery Order (DO) documents and a "Product Scan"
shelf-verification flow, but has **no concept of shelf stock/inventory at all** — no
batch, no expiry-per-batch, no quantity-on-hand. The Product Scan editor already has
a placeholder "batch/expiry" dropdown (`ProductExpiryCard`), but it's fake: it just
echoes the single OCR-extracted expiry-date string from the photo, with no real batch
code, no quantity, no link to what was actually delivered.
The user wants to close this gap: every confirmed DO should feed real, per-store,
per-SKU batch records (batch code + expiry + quantity, split across boxes/packs),
track where each batch came from, and let Product Scan consume from that real batch
pool (matching against it, and decrementing it) instead of operating on fabricated
data.
**Outcome**: a new Postgres schema + `/api/v1/stock/*` endpoints (backend), a new
mandatory-by-default "stock entry" step triggered right after DO confirmation, a new
"Stok" menu (Flutter), a rewired Product Scan batch-matching flow, and a basic
read-only admin web view.
## Confirmed decisions (resolved via one-at-a-time grilling, do not re-litigate)
- **Full-stack, per-store** (`kode_toko`) stock. Batch = unique `(kode_toko, no_sku,
batch_code, expiry_date)`; repeat deliveries of the same combo **merge** (quantity
adds), never duplicate.
- Batches track **both** outer qty (boxes/karung, matches DO's `banyak`) and inner
qty (packs/pieces, matches DO's `jumlah`) — both **manually typed**, seeded from
the DO item's already-user-corrected quantities (physical verification already
happens in the existing DO editor before confirmation; no re-verification here).
- **Every quantity change is logged** in an append-only movement table
(`intake`/`decrement`/`adjustment`/`manual_seed`), referencing the causing
document where applicable.
- **DO confirm trigger**: right after `PUT /api/v1/documents/:id` succeeds in the DO
editor (`editor_logic.dart`), auto-navigate to a stock-entry screen. Each item
**starts with 1 pre-filled batch** (full confirmed quantity); user can split into
more via "+ Tambah Batch". An explicit **"Isi Nanti"** (finish later) exits without
hard-blocking, leaving that DO resumable/visible from the Stocks menu.
- Batch code is **always manually typed** (no OCR/camera in stock-entry — plain form
only).
- **Stocks menu** ("Stok" in the drawer): two-level SKU list → batch detail, sorted
soonest-expiry-first, expired batches flagged red (visual only, no write-off
workflow this pass). Batches are manually editable after creation (logged as
`adjustment`). Manual "add batch" with no DO link is supported (seeds pre-existing
inventory, logged as `manual_seed`).
- **Product Scan integration**: SKU candidates restricted to in-stock (qty > 0)
SKUs only. Batch dropdown shows all of that SKU's in-stock batches (no cap);
closest-to-OCR-extracted-expiry batch is auto-selected. Depleted/negative batches
still selectable with a warning. Confirming decrements the selected batch's
**inner qty by exactly 1**, allowed to go **negative**, logged. **If the selected
batch id is invalid/cross-store, the whole document confirm fails (400)** — never
a silent skip.
- **Admin web**: read-only "Stock" tab in the existing `admin/master-data` surface,
all-stores view (admin bypasses `kode_toko` scoping).
- Backend work tracked in `backend/plans/next-enhancements.md` (new section);
Flutter work tracked in root `plans/next-enhancements.md` (new section) — both as
ad-hoc-originated backlog per `AGENTS.md` §7, not a fresh `e`/`enhance` pass.
## Backend design
### 1. Schema — new `backend/pfm-web-app/src/db/init-stock.ts`
```sql
CREATE TABLE IF NOT EXISTS stock_batches (
id SERIAL PRIMARY KEY,
kode_toko VARCHAR(255) NOT NULL REFERENCES store_master(kode_toko),
no_sku VARCHAR(255) NOT NULL REFERENCES sku_master(no_sku),
batch_code VARCHAR(255) NOT NULL,
expiry_date DATE NOT NULL,
outer_qty INTEGER NOT NULL DEFAULT 0,
inner_qty INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (kode_toko, no_sku, batch_code, expiry_date)
);
CREATE INDEX IF NOT EXISTS idx_stock_batches_store_sku ON stock_batches(kode_toko, no_sku);
CREATE TABLE IF NOT EXISTS stock_movements (
id SERIAL PRIMARY KEY,
batch_id INTEGER NOT NULL REFERENCES stock_batches(id) ON DELETE RESTRICT,
document_id INTEGER REFERENCES documents(id) ON DELETE SET NULL,
movement_type VARCHAR(20) NOT NULL CHECK (movement_type IN ('intake','decrement','adjustment','manual_seed')),
outer_delta INTEGER NOT NULL DEFAULT 0,
inner_delta INTEGER NOT NULL DEFAULT 0,
note VARCHAR(500),
created_by_account_id INTEGER REFERENCES accounts(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_stock_movements_batch ON stock_movements(batch_id);
CREATE INDEX IF NOT EXISTS idx_stock_movements_document ON stock_movements(document_id);
```
No soft-delete/DELETE route — batches are edit-only, matching "manually editable,
never removed." No `CHECK (qty >= 0)` — decrements must go negative. Wrap in
`try/catch` + `console.error`, matching `init.ts`'s existing idiom for every other
table. Wire into `init.ts`: `import { initStockSchema } from "./init-stock"; await
initStockSchema(pool);` right after the accounts table's plaintext-password
migration block (~line 209, before the `arena_runs` table), since `stock_batches`
FKs both `store_master` and `sku_master`, both of which must already exist. This is
itself the AGENTS.md §3-compliant "split" — new logic lives in a fresh sub-256-line
file rather than growing the already-over-budget `init.ts` (562 lines) further.
### 2. New utils
- **`src/utils/stock-mapper.ts`** — `mapStockSummaryRow`, `mapStockBatchRow`,
mirroring `document-mapper.ts`'s typed-row → response-shape pattern.
- **`src/utils/stock-movement.ts`** — `recordStockMovement(client, {batchId,
documentId, movementType, outerDelta, innerDelta, note})` (single INSERT);
`decrementBatchForProductScan(client, {batchId, kodeToko, documentId})` —
`SELECT ... FOR UPDATE`, store-isolation check, `inner_qty = inner_qty - 1` (no
floor), logs a `decrement` movement, returns `{ok:true}` or `{ok:false, reason}`.
- **`src/utils/stock-lookup.ts`** — `getInStockSkuSet(kodeToko)` (SKUs with
`SUM(inner_qty) > 0`); `filterMatchesByStock(matches, inStock)` for the classifier
candidate filter.
### 3. New routes under `src/app/api/v1/stock/`
- **`stock/route.ts`** — `GET` (summary per SKU: total outer/inner qty, batch
count, nearest expiry, `has_expired` flag; non-admin forced to own `kode_toko`,
admin sees all stores via optional `?kode_toko=`); `POST` (create/merge a batch —
`{kode_toko?, no_sku, batch_code, expiry_date, outer_qty, inner_qty,
document_id?}`, `withTransaction`: `INSERT ... ON CONFLICT
(kode_toko,no_sku,batch_code,expiry_date) DO UPDATE SET outer_qty = outer_qty +
EXCLUDED.outer_qty, inner_qty = inner_qty + EXCLUDED.inner_qty` then
`recordStockMovement` with type `intake` if `document_id` present else
`manual_seed`).
- **`stock/[noSku]/route.ts`** — `GET` batch list for one SKU (store-scoped),
sorted by `expiry_date ASC`.
- **`stock/batches/[id]/route.ts`** — `PUT` edit a batch (`{batch_code?,
expiry_date?, outer_qty?, inner_qty?, note?}`), `FOR UPDATE`, computes deltas,
updates, logs `adjustment`; 403 on cross-store, 409 on unique collision.
All follow existing conventions: `getAccountFromAuthHeader`, inline
`NextResponse.json({status:"success", data:...})` (no `successResponse` helper
exists in this codebase — don't invent one), `errorResponse()` from
`utils/api-error.ts`.
### 4. Decrement integration — `documents/[id]/route.ts`
- Extend the existing `checkRes` SELECT to also fetch `scan_mode`.
- Accept new top-level PUT body field `stock_batch_id: number | null`.
- Inside the **existing** `withTransaction` block (reuse it — don't open a second
transaction), after the `ocr_items` rewrite: if `doc.scan_mode === 'Product' &&
stock_batch_id`, call `decrementBatchForProductScan`; on `{ok:false}`, `throw` a
tagged error so the transaction rolls back, and the route's catch returns **400**
(not the current 500) for this specific case.
- Net effect: document confirm and stock decrement are atomic — per the confirmed
"fail the whole confirm on bad batch id" decision.
### 5. Product Scan candidate filtering
- `api/parse/route.ts`'s Product branch and `api/v1/scan-product/route.ts`: after
calling `classifyAndMatchProduct()`, filter `possibleMatches` through
`filterMatchesByStock(matches, await getInStockSkuSet(kodeToko))`. **Do not
change `classifyAndMatchProduct`'s signature** — it's shared with the anonymous,
store-agnostic desktop dev route; filter at the two authenticated call sites
instead.
### 6. Admin web view
- Split `admin/master-data/page.tsx` (419 lines, already over threshold) into
`page.tsx` (shell + login + tabs) + extracted `StoreManager.tsx` +
`SkuManager.tsx` (pure extraction, no behavior change) + new `StockManager.tsx`
(read-only: fetches `GET /api/v1/stock` with no `kode_toko` param as admin →
all-store table; row click expands a batch sub-table via `GET
/api/v1/stock/:noSku?kode_toko=...`).
## Flutter design
### 7. Data layer (new `lib/features/stock/`)
- `lib/models/stock_model.dart` — `StockBatch` (id, noSku, batchCode, expiryDate,
outerQty, innerQty, `isExpired`/`isDepleted` getters), `StockItem` (summary row:
noSku, namaItem, kodeToko?, totals, batchCount, nearestExpiryDate, hasExpired).
- `stock_response_parser.dart` — pure envelope unwrap (`{status,data}` → typed
lists), mirroring `product_scan_response_parser.dart`.
- `stock_batch_sort.dart` — pure `sortBatchesForDisplay()` (expired-first, then
soonest-expiry) and `pickClosestBatch(batches, extractedExpiry)`.
- `quantity_parse.dart` — pure `parseLeadingQty(String)` for `"10 KRG"` → `10`
style extraction, used to seed the stock-entry screen's default pre-filled
quantities from `DocumentItem.banyak`/`.jumlah`.
- `stock_provider.dart` — `StockNotifier extends StateNotifier<List<StockItem>>` +
`stockProvider`, matching `pending_documents_provider.dart`'s conventions
(network via `ref.read(apiClientProvider)`, no premature caching abstractions).
- `app_config.dart` — add `stockEndpoint = '/stock'`.
- `local_storage.dart` — new `stockEntryBoxName` Hive box (draft persistence for
"Isi Nanti" resume), registered in `init()`, wiped in `clearAll()`.
### 8. Stock-entry screen (new `lib/features/stock/`)
`part of` + mixin split (matching both existing editor screens' convention):
- `stock_entry_screen.dart` — shell, receives `DocumentModel` via `state.extra`.
- `stock_entry_logic.dart` (`part of`) — per-item batch-draft state, Hive draft
load/resume, `batchesSumMatches()` validation (pure function, own test file).
- `stock_entry_submit_logic.dart` (`part of`) — per-item submit
(`stockProvider.createOrMergeBatch(..., documentId: document.id)`, idempotency
via a `submitted` flag per batch draft so resume never double-POSTs), "Isi Nanti"
(persist + pop), "Selesai" (clear draft + pop).
- `widgets/stock_entry_item_card.dart`, `widgets/stock_entry_batch_form.dart` (also
reused by the Stocks menu's manual-add/edit).
**`editor_logic.dart`** — on `syncedToServer == true`, replace `context.pop(true)`
with `context.pushReplacement('/stock-entry', extra: finalDoc)`; extract the branch
into a new pure `lib/features/editor/document_submit_navigation.dart`
(`resolveDocumentSubmitNavigation({required bool syncedToServer})`), matching the
`resolveDocumentSaveAction`/`resolvePollOutcome` pattern already used twice in this
codebase. Keeps `editor_logic.dart` under 256 lines and testable without widget
mocking.
**`app_router.dart`** — add `/stock-entry` (`extra: DocumentModel`), `/stocks`,
`/stock-detail` (`extra: StockItem`).
### 9. Stocks menu (new `lib/features/stock/`)
- `stocks_screen.dart` — "Perlu diisi" banner (from Hive drafts, tap-to-resume),
searchable SKU list.
- `stock_detail_screen.dart` — batch list via `sortBatchesForDisplay`, expired-red
flag, tap-to-edit (bottom sheet, reuses `stock_entry_batch_form.dart`), FAB "+
Tambah Batch Manual" (`documentId: null` → `manual_seed`).
- `camera_drawer.dart` — new `buildDrawerItem(icon: Icons.inventory_2_outlined,
title: 'Stok', onTap: () { Navigator.pop(context); context.push('/stocks'); })`
right after "History".
### 10. Product Scan rewrite
- `product_editor_data_logic.dart` — `_skuBatches` becomes `Map<String,
List<StockBatch>>`; classifier-matched-SKU branch additionally fetches real batch
data per candidate (`stockProvider.fetchBatchDetail`); fallback branch switches
from `masterSkusEndpoint` to `stockEndpoint` (filtered to qty > 0) — this
**removes** a code path rather than adding one, and removes the manual-date-entry
fallback entirely (no longer a valid state once candidates are stock-guaranteed to
have ≥1 batch).
- `product_editor_submit_logic.dart` — add `selectedStockBatchId` to
`finalDoc`/`toPutPayload()` as `stock_batch_id`.
- `product_expiry_card.dart` — rewritten props (`List<StockBatch> batches`,
`StockBatch? selectedBatch`), depleted-batch warning row. Update
`test/product_expiry_card_test.dart` for the new shape (breaking change, not
additive).
- `document_model.dart` — add `selectedStockBatchId` field.
## Sequencing (7 chunks, backend → Flutter → admin)
1. Backend schema + core CRUD (`init-stock.ts`, `stock-mapper.ts`,
`stock-movement.ts`'s `recordStockMovement`, all 3 stock routes). Verify live:
merge-add, 409 on collision, correct movement rows.
2. Backend decrement hook + in-stock filter (`decrementBatchForProductScan`,
`documents/[id]/route.ts` wiring, `stock-lookup.ts`, `parse/route.ts` +
`scan-product/route.ts` filtering). Verify live: decrement to negative, 400 on
bad batch id, filtered candidates.
3. Flutter data layer (models, parser, sort/qty pure functions, provider, Hive
box, config). Unit tests only.
4. Flutter stock-entry screen + DO-confirm integration (`editor_logic.dart`,
`document_submit_navigation.dart`, router). Live-verify: DO confirm →
stock-entry → submit → Postgres rows; "Isi Nanti" resumability.
5. Flutter Stocks menu (list/detail screens, drawer, manual add/edit).
Live-verify search, expiry sort/flag, manual seed, edit.
6. Flutter Product Scan rewrite (`product_editor_data_logic.dart`,
`product_editor_submit_logic.dart`, `product_expiry_card.dart`, updated test).
Live-verify: depleted SKU disappears from candidates, closest-expiry auto-pick,
decrement (including going negative).
7. Admin web (`page.tsx` split + `StoreManager.tsx`/`SkuManager.tsx` extraction +
new `StockManager.tsx`). Live-verify cross-store view.
Backend chunks (1-2, 7's backend half) are tracked as backend/plans
`§12`; Flutter chunks (3-6) are tracked as root plans `§9`. Each task flips
`[TODO]` → `[DONE]` and gets logged in the relevant `docs/feature-list.md` as it
ships, per the normal `n`/`next` workflow.
## Testing plan
**Dart unit tests** (pure functions, no Dio mocking — this repo's established
convention): `stock_response_parser_test.dart`, `stock_batch_sort_test.dart`
(ordering + `pickClosestBatch`), `quantity_parse_test.dart`,
`document_submit_navigation_test.dart`, `stock_entry_sum_validation_test.dart`.
Update `product_expiry_card_test.dart` for the new prop shape.
**Backend**: no existing route-test harness — this repo relies on live
verification against the running Docker stack (per established practice). For
each chunk: `docker compose up -d --build`, curl every new endpoint as both a
store account and admin, confirm merge/409/movement-row correctness, then a real
end-to-end pass through the actual Flutter app (DO scan → confirm → stock-entry →
submit; Product Scan → depleted-SKU exclusion → decrement) with Postgres row
checks, matching how prior tasks (6.1–8.2, 9.x, 10.x, 11.1) were verified in this
repo's iteration logs.
## Open implementation-time decisions (flagged during planning, not yet re-confirmed)
- Whether an invalid/cross-store `stock_batch_id` should ever be *allowed* to skip
silently instead of failing the whole confirm — resolved: **fail the whole
confirm (400)**, per user answer during grilling.
- Stock-entry screen default state (ask batch count upfront vs. start with 1
pre-filled batch) — resolved: **start with 1 pre-filled, "+ Tambah Batch" to
split**, per user answer during grilling.
+95 -4
View File
@@ -53,6 +53,10 @@ When complete, the status flips to `[DONE]` and the feature is logged in
- **1.2** [TODO] Add a global 401/403 response interceptor to the Dio client that force-logs-out and redirects to `/login`, so an expired/revoked token surfaces as a clear re-login prompt instead of failing whatever screen happens to make the next API call.
- **1.3** [DONE 2026-07-10] Warn before logout if the pending documents queue (`pendingDocumentsProvider`) has unsynced items. *(File reference corrected: the drawer now lives in `lib/features/camera/camera_drawer.dart`, not `camera_screen.dart` — extracted in a later commit.)* Acceptance: if the queue is non-empty when Logout is tapped, show a confirm dialog ("Ada Dokumen Belum Tersinkron" / item count / Batal-or-Ya-Logout) before calling `logout()`; if empty, logout proceeds immediately as before. Implemented in `CameraDrawer._handleLogout()` (`camera_drawer.dart`). See docs/feature-list.md.
- **1.4** [TODO] Warn before login wipes local state, mirroring 1.3's logout guard. `AuthNotifier.login()` (`auth_provider.dart:62-65`) unconditionally calls `localStorage.clearAll()` and `pendingDocumentsProvider.clearQueue()` *before* saving the new session — if there's a non-empty pending/unsynced queue at the moment of login (re-login after a forced logout, or a shared device), it's silently destroyed with zero recovery path, unlike the logout path which now confirms first.
- **1.5** [TODO] Stop sequencing the splash screen's fixed delays ahead of the network-dependent auth check. `_checkAuthAndNavigate()` (`splash_screen.dart:32-43`) always waits the full 2s fade + a hardcoded extra 1s "for effect" *before even starting* `checkLoginState()`'s `/auth/me` round trip — every cold launch pays ~3s of artificial delay on top of real network latency. Run the animation and the auth check concurrently (`Future.wait`) and navigate as soon as both finish.
- **1.6** [TODO] Add an error boundary around the splash screen's auth check. `_checkAuthAndNavigate()` calls `await ref.read(authProvider.notifier).checkLoginState()` with no try/catch — an exception from `SharedPreferences.getInstance()` or elsewhere in that call chain propagates uncaught, and since neither `context.go('/camera')` nor `context.go('/login')` ever executes, the user is stuck on the splash screen indefinitely with no visible error and no way forward except force-closing the app.
### 2. Flutter — Camera Capture & Geotagging
`lib/features/camera/`
@@ -60,6 +64,10 @@ When complete, the status flips to `[DONE]` and the feature is logged in
- **2.2** [TODO] Show a location-fix quality/staleness indicator on the capture screen before the shutter is pressed. `_currentPosition` in `CameraScreen` (`camera_screen.dart:22,37-62`) is used whatever its age or accuracy, with no on-screen warning when no fix has landed yet or the fix is old — a document can silently upload with a poor or missing GPS tag.
- **2.3** [TODO] Replace the static "posisikan seluruh halaman dokumen di dalam foto" instructional text with a live document-alignment overlay during capture. `CameraScreen` only launches the OS's native camera app via `ImagePicker(source: ImageSource.camera)` (`camera_screen.dart:69-74`) — there's no in-app camera preview, so the framing guideline is shown once beforehand and then unavailable during the actual shot.
- **2.4** [TODO] Enforce the blur check instead of only displaying it. `ImagePreviewScreen`'s "Unggah Dokumen" button (`image_preview_screen.dart:286-294`) is gated only on `!_analyzing` — not on `!_blurResult!.isBlur` — so a photo flagged "Foto Terdeteksi Blur! Diharuskan untuk mengambil ulang gambar" can still be uploaded as-is. The blur badge is currently cosmetic.
- **2.5** [TODO] Flag or restrict gallery-sourced captures. `_pickFromGallery()` (`camera_screen.dart:96-144`) lets staff select any existing photo from the device gallery, not just a fresh capture — if it has no EXIF GPS and no live position is cached, it uploads with no geotag and nothing on the document distinguishes it later from a live camera capture, undermining the GPS tag's purpose as proof the staff was physically present.
- **2.6** [TODO] Surface camera/gallery picker failures to the user. `_takePicture()`/`_pickFromGallery()` (`camera_screen.dart:91-93,141-143`) catch all `image_picker` errors (permission denied, picker cancelled abnormally, IO failure) with only `debugPrint` — tapping "Ambil Foto Kamera"/"Pilih Dari Galeri" can silently do nothing with zero on-screen explanation of why.
### 3. Flutter — Pending Documents Queue
`lib/features/documents/`
@@ -67,6 +75,10 @@ When complete, the status flips to `[DONE]` and the feature is logged in
- **3.2** [TODO] Add a persistent "pending/syncing count" badge visible from the camera screen (not just the `/documents` list), reflecting `pendingDocumentsProvider` state — today a driver who navigates away from `/documents` gets no visibility into background uploads still in progress or stuck in `error`/`syncFailed`.
- **3.3** [TODO] Right-size per-request timeouts. *(Description refreshed 2026-07-10 — the original claim "no explicit connect/receive timeout" is stale: `ApiClient` now sets `connectTimeout` 10s / `receiveTimeout` 240s globally, `api_client.dart:14-19`.)* Remaining gap: the 240s receive timeout is sized for the upload's synchronous OCR pass but is inherited by *every* call — a hung 2s-interval poll `GET /documents` can stall one iteration for up to 4 minutes, and login/list calls hang far longer than useful. Pass tighter per-request `Options(receiveTimeout: ...)` on the poll/list/login paths, keeping the long timeout only where the slow parse justifies it. Coordinate with 5.1 (failover needs fast failure).
- **3.4** [TODO] Clean up orphaned captured-image files. Neither `removeDocument()` nor the success path in `_uploadAndProcess()` (`pending_documents_provider.dart`) ever deletes the underlying photo file at `imagePath` — only the Hive record is removed. Every synced or manually-dismissed document leaves its captured image on device storage indefinitely, growing unbounded over the app's lifetime.
- **3.5** [TODO] Stop reporting network failures as "OCR timeout." `_pollUntilParsed()`'s per-iteration catch (`pending_documents_provider.dart:142`) silently swallows every exception (including plain connectivity errors) and just retries; if all 130 retries are consumed this way, the final message is always the generic "Gagal mengekstrak data (Timeout)." — masking a bad Wi-Fi connection as a backend/OCR problem and making real issues harder to diagnose in the field.
- **3.6** [TODO] Throttle pending-queue processing on load instead of firing everything at once. `_loadPendingDocuments()` (`pending_documents_provider.dart:22-36`) starts an independent upload-or-poll loop for *every* non-terminal item simultaneously on provider construction — if a store queues many documents while offline and then reconnects, the app fires that many concurrent multipart uploads and 2s-interval polling loops at once against a single-GPU backend already flagged as a throughput bottleneck.
### 4. Flutter — Document Editor & PDF Receipt
`lib/features/editor/`
@@ -74,6 +86,10 @@ When complete, the status flips to `[DONE]` and the feature is logged in
- **4.2** [TODO] Block save when a line item's SKU isn't in the master registry, instead of only relabeling it for display. The SKU listener in `_addItem` (`editor_screen.dart:108-115`) sets the item name to "SKU Tidak Terdaftar" for an unrecognized SKU but doesn't stop form submission, so a document with an unregistered/mistyped SKU can still be saved and its receipt printed.
- **4.3** [TODO] Include the captured GPS coordinates on the printed PDF receipt. `PdfService.generateAndPrintReceipt` (`pdf_service.dart:36-52`) prints header/shipment/item fields but never includes `document.latitude`/`longitude`, even though the editor captures and displays them (`_latitudeCtrl`/`_longitudeCtrl`) — the geotag exists in the data model but isn't part of the audit-trail document a store keeps.
- **4.4** [TODO] Block save on a document with zero line items. `_showConfirmationDialog()` (`editor_logic.dart:94-117`) only runs `_formKey.currentState!.validate()` (per-field validators) — nothing checks `_itemControllers.isNotEmpty`. If every item is removed via `_removeItem`, the form still validates and the document saves/prints with an empty items list.
- **4.5** [TODO] Stop validating DO items against a static bundled SKU list. The item SKU field's validator (`items_list_card.dart:85`) checks membership in `MasterSku.data` — a hardcoded `Map` compiled into the app (`lib/data/master_sku.dart`) — instead of the live `/api/v1/master/skus` endpoint the Product Scan flow already uses (task 7.1). Any SKU added/changed centrally won't validate correctly in the DO editor until the app is rebuilt and redeployed to every device, and the bundled copy can silently drift from the real master data.
- **4.6** [TODO] Strengthen delivery confirmation beyond a typed name. `ConfirmationDialog` (`confirmation_dialog.dart`) accepts any freely-typed string as `namaPenerima` plus a checkbox — nothing verifies the person confirming is who they claim, yet that typed name is printed on the PDF receipt's signature line (`pdf_service.dart`) as if it were an actual signature. Consider a lightweight identity check (PIN re-entry, or a captured signature/initial) before treating it as a confirmed receipt.
### 5. Flutter — Connectivity & Endpoint Resolution
`lib/config/app_config.dart`, `lib/core/network/api_client.dart`
@@ -120,6 +136,10 @@ actually missing:
probe to it — probing `POST /auth/login` with an empty body works but couples
reachability to the login route's error shape.
- **5.4** [TODO] Stop unconditionally logging sensitive data. `LogInterceptor(requestBody: true, responseBody: true)` (`api_client.dart:44-47`) logs every full request/response body — including the `Authorization: Bearer` token and personal data (driver/receiver names, GPS coordinates, DO contents) — via Dio's default `print`, which is not gated behind `kDebugMode` and will also run in release builds, exposing tokens and PII to on-device logs (`adb logcat`) in the field.
- **5.5** [TODO] Add proactive connectivity-change detection to complement 5.1's reactive failover. There's no OS-level network-change listener (e.g. `connectivity_plus`) — the app only discovers a stale endpoint after a request already times out. Listening for WiFi↔cellular transitions would let it re-resolve the base URL immediately instead of waiting for the next failed call.
- **5.6** [TODO] Encrypt the LAN leg of traffic. `_lanBaseUrl` (`app_config.dart:22`) is plain `http://`, meaning the bearer token and all document contents travel unencrypted over the store's local Wi-Fi whenever the LAN endpoint is active — anyone else on that network can passively sniff credentials or DO data. The ngrok leg is already HTTPS; the LAN leg has no equivalent protection.
### 6. Flutter — API Contract & Sync Integrity (DO flow)
`lib/features/documents/`, `lib/models/document_model.dart`, `lib/core/network/`
@@ -217,6 +237,10 @@ generally must ship first.
GET confirms the correction persisted), proving the recovery path is a
genuine save, not a dead end. See docs/feature-list.md.
- **6.4** [TODO] Fix `DocumentModel.toJson()` silently dropping fields on every local persist. `toJson()` (`document_model.dart:124-153`), used for *all* Hive writes (pending queue and the confirmed `documentBox`, including `documents_screen.dart`'s clear-and-resave on every refresh), omits `confirmed`, `parseStatus`, `productScanMatches`, and `productScanExtractedExpiryDate`. Any document reloaded from local cache silently reverts `confirmed` to its default `true` and loses task 7.2's stored classification data, defeating both the confirmation gate and the "don't reclassify" optimization specifically in the cached/offline path.
- **6.5** [TODO] Add an offline/stale-data indicator to the documents list. `_loadDocuments()`'s network fetch failure (`documents_screen.dart:87-89`) is swallowed with only a `debugPrint` — a store on a dead connection sees the same "Dokumen" list with no on-screen indication it might be showing stale cached data rather than the current server state.
- **6.6** [TODO] Extend document search to item contents. `_onSearchChanged()` (`documents_screen.dart:92-103`) filters only on header fields (`noDo`/`noPo`/`noSo`/`tanggal`/`kepadaYth`) — a staff member searching for a document by product/SKU name gets no results, even though that's a natural way to look up "that frozen chicken delivery from last week."
### 7. Flutter — Product Scan Review Flow
`lib/features/editor/product_editor_screen.dart` + `product_editor_logic.dart` + `widgets/product_*`, `lib/features/camera/scan_mode_provider.dart`
@@ -343,6 +367,10 @@ endpoints and placeholder data.
with `docType: 'Product'` but an edited, non-matching `orderUntuk` still
renders as Product). See docs/feature-list.md.
- **7.4** [TODO] Stop submitting hardcoded placeholder fields as real data. `_submit()` (`product_editor_submit_logic.dart:76,81`) sends `noSo: '1002003004'` and `platTruk: 'B 1234 PFM'` as literal constants on *every* Product Scan document — these make sense for a real DO but are meaningless here, yet they're persisted server-side as if real. Either omit/null them for `docType: 'Product'` or replace with fields that actually apply to a product scan.
- **7.5** [TODO] Fix the store-name fallback that can misattribute data to the wrong store. `currentStoreName` (`product_editor_submit_logic.dart:13`) falls back to the hardcoded literal `'PM KELAPA DUA KARAWACI'` (a real store name) if the cached `nama_toko` preference is ever missing — silently attributing a scan to a different physical store instead of surfacing an error or blocking submission.
- **7.6** [TODO] Guard `_submit()` against double-tap. `product_editor_screen.dart:101` wires `onPressed: _submit` directly with no in-flight/loading guard — unlike other async actions in the app, nothing disables the button while a submission (including the re-upload path) is still running, so a fast double-tap can fire two concurrent submissions.
### 8. Flutter — Scan Mode UX & Confirmation Gate
`lib/features/camera/scan_mode_provider.dart`, `lib/features/documents/`, `lib/config/app_config.dart`, `lib/models/document_model.dart`
@@ -399,12 +427,75 @@ root-cause documentation. §8 is now fully `[DONE]`.
analyze` clean. Live-verified against the real backend response shape
(see backend `docs/iteration-log.md`'s task 10.1/10.2 entry).
- **8.3** [TODO] Persist the active scan mode across app restarts. `scanModeProvider` (`scan_mode_provider.dart:3`) is a plain in-memory `StateProvider<String>((ref) => 'DO')` — every cold launch resets to 'DO' regardless of which mode the staff was last using, even for a store role that primarily does Product Scans.
- **8.4** [TODO] Add a confirmation step before deleting a pending document. Both the `error` and `syncFailed` bottom sheets in `PendingDocumentCard` (`pending_document_card.dart:76-83,102-109`) wire "Hapus Dokumen" straight to `removeDocument()` with no confirm dialog — unlike the 1.3 logout guard, a single mistap permanently discards a captured photo and any OCR/edit progress with no undo.
- **8.5** [TODO] Surface how long a document has been awaiting confirmation. A `success`-status pending item (OCR done, waiting for the user to tap through and confirm) has no staleness/aging indicator — it can sit unconfirmed indefinitely with no on-screen reminder, risking goods being shelved before the DO is ever formally confirmed.
### 9. Flutter — Stocks Menu & DO-to-Stock Flow
`lib/features/stock/` (new), `lib/features/editor/editor_logic.dart`,
`lib/features/editor/product_editor_*`, `lib/core/router/app_router.dart`,
`lib/features/camera/camera_drawer.dart`
Added 2026-07-10 from a user-directed, extensively grilled ad-hoc feature request
(not an `e`/`enhance` section — see AGENTS.md §7). **Read
[docs/stock-feature-plan.md](../docs/stock-feature-plan.md) first** — it is the
full design doc (schema, API contracts, screen/file layout, sequencing, testing
plan) for this section and for backend counterpart
`backend/plans/next-enhancements.md` §12. **Status: planned, not yet
implemented** — no code for this feature exists in the codebase yet.
- **9.1** [TODO] **Stock data layer.** New `lib/models/stock_model.dart`
(`StockBatch`/`StockItem`), `lib/features/stock/stock_response_parser.dart`,
`stock_batch_sort.dart`, `quantity_parse.dart` (pure functions, unit tested),
`stock_provider.dart` (`StateNotifier` per `pending_documents_provider.dart`
convention), new `stockEndpoint` in `app_config.dart`, new `stockEntryBoxName`
Hive box in `local_storage.dart`. Blocked on backend §12.1 shipping the
`/api/v1/stock/*` routes this layer calls.
- **9.2** [TODO] **Stock-entry screen + DO-confirm trigger.** New
`lib/features/stock/stock_entry_screen.dart` (+ `stock_entry_logic.dart`/
`stock_entry_submit_logic.dart` part files, widgets) — the mandatory-by-default
screen auto-navigated to right after a DO's `PUT /api/v1/documents/:id`
succeeds (`editor_logic.dart`'s `_submitDocument()`), pre-filling each item
with 1 batch at its confirmed quantity, "+ Tambah Batch" to split, "Isi
Nanti" to defer (Hive-persisted, resumable). New pure
`lib/features/editor/document_submit_navigation.dart` (mirrors
`resolveDocumentSaveAction`/`resolvePollOutcome`) so `editor_logic.dart`
stays under the 256-line threshold. New `/stock-entry` route in
`app_router.dart`. Blocked on 9.1.
- **9.3** [TODO] **Stocks menu (list/detail) + drawer entry.** New
`lib/features/stock/stocks_screen.dart` (searchable SKU list + "Perlu diisi"
resumable-draft banner) and `stock_detail_screen.dart` (batch list,
soonest-expiry-first, red "Kadaluwarsa" flag, manual add/edit batch actions).
New "Stok" item in `camera_drawer.dart`, `/stocks`/`/stock-detail` routes in
`app_router.dart`. Blocked on 9.1.
- **9.4** [TODO] **Product Scan rewrite onto real stock batches.** Replace
`product_editor_data_logic.dart`'s fake `_skuBatches` (a single OCR-echoed
date string) with real per-store batch data from 9.1's provider; SKU
candidates restricted to in-stock SKUs (backend §12.2 filter); closest-to-
OCR-expiry batch auto-selected; depleted batches shown with a warning, not
hidden. `product_editor_submit_logic.dart` sends the selected batch id
(`stock_batch_id`) on PUT so backend §12.2's decrement hook fires.
`product_expiry_card.dart` prop-shape rewrite (breaking change — update
`test/product_expiry_card_test.dart`, don't leave it stale). New
`selectedStockBatchId` field on `DocumentModel`. Blocked on 9.1 and backend
§12.2.
- **9.5** [TODO] Define draft-recovery behavior for a killed app mid-stock-entry. The plan covers explicit "Isi Nanti" (finish-later) resumability, but not what happens if the app is killed *while* the stock-entry form is open, before that button is tapped — the same class of loss already fixed for the pending-upload queue (task 3.1). Specify whether the form autosaves per-field to Hive or only on an explicit exit action, so typed batch/quantity data isn't silently lost.
- **9.6** [TODO] Resolve the SKU master staleness risk before it hits stock intake. Since the DO editor currently validates SKUs against a static bundled `lib/data/master_sku.dart` map instead of the live `/api/v1/master/skus` endpoint (task 4.5), a SKU that exists in the backend's `sku_master` table but not yet in the bundled copy would fail client-side validation during DO entry — blocking that item from ever reaching stock intake even though the backend would accept it. Fix 4.5 before or alongside 9.1.
- **9.7** [TODO] Require a reason for manual, DO-unlinked stock adjustments. `stock_movements.note` (`init-stock.ts`'s schema) is an optional, nullable `VARCHAR(500)` — DO-triggered movements are always tied to a real `document_id`, but a manual "Tambah Batch" (`manual_seed`/`adjustment`, no DO link) can be submitted with no justification at all, leaving no mandatory audit trail for a fabricated or corrected stock quantity.
*Suggested order: 9.1 → 9.2/9.3 (independent of each other once 9.1 lands) →
9.4 (needs backend §12.2's decrement hook, not just §12.1's CRUD). 9.5-9.7 are
design-completeness fixes to fold into 9.1-9.4's implementation, not a separate pass.*
---
*Sections 1-8 (Flutter) are the only sections this file tracks. Backend
*Sections 1-9 (Flutter) are the only sections this file tracks. Backend
enhancements (formerly sections 5-8 here, removed 2026-07-08) now live
exclusively in [backend/plans/next-enhancements.md](../backend/plans/next-enhancements.md);
that file's §9 holds the backend counterparts to this file's §6-7, and §10
holds the backend counterparts to this file's §8 (see
[docs/api-contract-map.md](../docs/api-contract-map.md) for the shared gap IDs).*
that file's §9 holds the backend counterparts to this file's §6-7, §10
holds the backend counterparts to this file's §8, and §12 holds the backend
counterparts to this file's §9 (see
[docs/api-contract-map.md](../docs/api-contract-map.md) and
[docs/stock-feature-plan.md](../docs/stock-feature-plan.md) for the shared
design docs).*