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

@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../config/app_config.dart';
import 'scan_mode_provider.dart';
/// DO / Product Scan mode toggle pill rendered in the [CameraDrawer].
///
/// Extracted from [CameraDrawer] to keep that file under the 256-line §3
/// threshold (AGENTS.md §3, touched during task 8.1). Reads and writes
/// [scanModeProvider] directly — the global source of truth shared with
/// [DocumentsScreen] (task 8.1, docs/api-contract-map.md G11).
///
/// Color convention:
/// - DO active → [AppConfig.doModeColor] (orange)
/// - Product active → [AppConfig.primaryColor] (green)
class CameraDrawerModeToggle extends ConsumerWidget {
const CameraDrawerModeToggle({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final activeMode = ref.watch(scanModeProvider);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mode',
style: AppConfig.bodySmall.copyWith(
color: Colors.grey.shade500,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Container(
height: 48,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
children: [
_buildSegment(
ref: ref,
value: 'DO',
label: 'DO Scan',
activeIcon: Icons.description,
inactiveIcon: Icons.description_outlined,
activeMode: activeMode,
activeColor: AppConfig.doModeColor,
),
_buildSegment(
ref: ref,
value: 'Product',
label: 'Product Scan',
activeIcon: Icons.inventory_2,
inactiveIcon: Icons.inventory_2_outlined,
activeMode: activeMode,
activeColor: AppConfig.primaryColor,
),
],
),
),
),
],
),
);
}
Widget _buildSegment({
required WidgetRef ref,
required String value,
required String label,
required IconData activeIcon,
required IconData inactiveIcon,
required String activeMode,
required Color activeColor,
}) {
final isActive = activeMode == value;
return Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => ref.read(scanModeProvider.notifier).state = value,
child: Container(
decoration: BoxDecoration(
color: isActive ? activeColor : Colors.transparent,
borderRadius: BorderRadius.circular(8),
boxShadow: isActive
? [
BoxShadow(
color: activeColor.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
)
]
: [],
),
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isActive ? activeIcon : inactiveIcon,
size: 16,
color: isActive ? Colors.white : Colors.grey.shade500,
),
const SizedBox(width: 6),
Text(
label,
style: AppConfig.bodyMedium.copyWith(
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
color: isActive ? Colors.white : Colors.grey.shade600,
),
),
],
),
),
),
);
}
}