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:
Rafhan Mazaya FathurrahmanandClaude Sonnet 5 committed 2026-07-10 15:19:32 +07:00
1 parent 2febe0c886
commit ada6488592
67 files changed
+5705 -1142

No files matched your search

+21 -9
View File
@@ -6,33 +6,45 @@ import '../../models/document_model.dart';
class DocumentCard extends StatelessWidget {
final DocumentModel doc;
final Future<void> Function(DocumentModel) onPrint;
final bool isUnsynced;
const DocumentCard({
super.key,
required this.doc,
required this.onPrint,
this.isUnsynced = false,
});
@override
Widget build(BuildContext context) {
final isProductScan = doc.orderUntuk == 'PRODUCT SCAN';
final isProductScan = doc.docType == 'Product';
// Header category text
final categoryText = isProductScan
? (doc.items.isNotEmpty ? doc.items.first.namaBarang : 'Product Scan')
: (doc.namaPenerima.isNotEmpty ? doc.namaPenerima : 'Staff Toko');
// Category color (greenish-blue for product, orange-yellow for DO)
final categoryColor = isProductScan ? AppConfig.primaryColor : Colors.orange.shade700;
// Category color: Product → green (primaryColor), DO → orange (doModeColor).
// doModeColor is the shared constant for the DO orange cue across the app
// (camera drawer, tab switcher, and here) — was an ad-hoc inline value.
final categoryColor = isProductScan ? AppConfig.primaryColor : AppConfig.doModeColor;
// Sync status badge (see docs/api-contract-map.md G6 - a document can be
// corrected locally after its PUT sync failed; the corrected copy is
// shown here, so the badge must reflect that it hasn't reached the
// server yet rather than always claiming "Terkonfirmasi").
final badgeLabel = isUnsynced ? 'Belum Tersinkron' : 'Terkonfirmasi';
final badgeColor = isUnsynced ? Colors.deepOrange : AppConfig.successColor;
// Bold title
final boldTitle = isProductScan
? doc.noDo
: doc.noPo;
// Subtitle text
// Subtitle text. Product Scan no longer links to a PO document (field
// removed - user request), so its subtitle is just the expiry date.
final subtitleText = isProductScan
? '${_formatDateString(doc.alamat.replaceAll("Product Scan: Expired ", ""))} • ${doc.noPo}'
? _formatDateString(doc.alamat.replaceAll("Product Scan: Expired ", ""))
: '${doc.tanggal} • ${doc.items.length} Item';
return Card(
@@ -92,17 +104,17 @@ class DocumentCard extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Status badge (e.g. Approved / Terkonfirmasi)
// Status badge (Terkonfirmasi / Belum Tersinkron)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppConfig.successColor.withOpacity(0.1),
color: badgeColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
),
child: Text(
'Terkonfirmasi',
badgeLabel,
style: AppConfig.bodySmall.copyWith(
color: AppConfig.successColor,
color: badgeColor,
fontWeight: FontWeight.bold,
fontSize: 11,
),