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
@@ -2,7 +2,6 @@ export 'pending_document_model.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../models/document_model.dart';
|
||||
import '../../core/location/location_service.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
@@ -11,6 +10,7 @@ import '../../config/app_config.dart';
|
||||
import '../../main.dart';
|
||||
import '../camera/scan_mode_provider.dart';
|
||||
import 'pending_document_model.dart';
|
||||
import 'poll_outcome.dart';
|
||||
|
||||
class PendingDocumentsNotifier extends StateNotifier<List<PendingDocument>> {
|
||||
PendingDocumentsNotifier(this._ref) : super([]) {
|
||||
@@ -82,52 +82,16 @@ class PendingDocumentsNotifier extends StateNotifier<List<PendingDocument>> {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final scanMode = _ref.read(scanModeProvider);
|
||||
if (scanMode == 'Product') {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
final todayStr = DateFormat('dd MMMM yyyy').format(DateTime.now());
|
||||
final dummyDoc = DocumentModel(
|
||||
id: id,
|
||||
tanggal: todayStr,
|
||||
noPo: 'PO-PRODUCT-001',
|
||||
noSo: '1002003004',
|
||||
noDo: 'DO-PRODUCT-999',
|
||||
kepadaYth: 'PM KELAPA DUA KARAWACI',
|
||||
orderUntuk: 'REPLENISHMENT SKU',
|
||||
alamat: 'Jl. Ancol Barat VIII no.1, Jakarta Utara',
|
||||
platTruk: 'B 1234 PFM',
|
||||
namaDriver: 'DUMMY DRIVER (PRODUCT SCAN)',
|
||||
namaPenerima: '',
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
items: [
|
||||
DocumentItem(
|
||||
nomorSku: '12010119',
|
||||
namaBarang: 'FIESTA SPICY CHICKEN NUGGET 500 GR',
|
||||
banyak: '10',
|
||||
jumlah: '10',
|
||||
),
|
||||
DocumentItem(
|
||||
nomorSku: '12012504',
|
||||
namaBarang: 'AKUMO NUGGET COIN 200 GR',
|
||||
banyak: '5',
|
||||
jumlah: '5',
|
||||
),
|
||||
],
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.success, document: dummyDoc));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final scanMode = _ref.read(scanModeProvider);
|
||||
final apiClient = _ref.read(apiClientProvider);
|
||||
final formData = FormData.fromMap({
|
||||
'image': await MultipartFile.fromFile(imagePath, filename: imagePath.split(RegExp(r'[/\\]')).last),
|
||||
if (lat != null) 'latitude': lat.toString(),
|
||||
if (lng != null) 'longitude': lng.toString(),
|
||||
'scan_mode': scanMode,
|
||||
});
|
||||
|
||||
final response = await apiClient.client.post(AppConfig.uploadDocumentEndpoint, data: formData);
|
||||
@@ -153,23 +117,26 @@ class PendingDocumentsNotifier extends StateNotifier<List<PendingDocument>> {
|
||||
|
||||
Future<void> _pollUntilParsed(String id, DocumentModel doc) async {
|
||||
final apiClient = _ref.read(apiClientProvider);
|
||||
bool isParsed = false;
|
||||
DocumentModel? parsedDoc;
|
||||
DocumentModel? resultDoc;
|
||||
String? failureMessage;
|
||||
int retries = 0;
|
||||
const maxRetries = 130;
|
||||
|
||||
while (!isParsed && retries < maxRetries && mounted) {
|
||||
while (retries < maxRetries && mounted) {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
final getRes = await apiClient.client.get(AppConfig.fetchDocumentsEndpoint);
|
||||
if (getRes.statusCode == 200 && getRes.data['data'] is List) {
|
||||
final list = (getRes.data['data'] as List).map((e) => DocumentModel.fromJson(Map<String, dynamic>.from(e))).toList();
|
||||
final found = list.firstWhere((element) => element.id == doc.id, orElse: () => doc);
|
||||
if (found != doc) {
|
||||
isParsed = true;
|
||||
parsedDoc = found;
|
||||
final getRes = await apiClient.client.get('${AppConfig.fetchDocumentsEndpoint}/${doc.id}');
|
||||
if (getRes.statusCode == 200 && getRes.data['data'] != null) {
|
||||
final fetched = DocumentModel.fromJson(Map<String, dynamic>.from(getRes.data['data']));
|
||||
final outcome = resolvePollOutcome(fetched);
|
||||
if (outcome.kind == PollOutcomeKind.done) {
|
||||
resultDoc = outcome.document;
|
||||
break;
|
||||
} else if (outcome.kind == PollOutcomeKind.failed) {
|
||||
failureMessage = outcome.message;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -177,10 +144,10 @@ class PendingDocumentsNotifier extends StateNotifier<List<PendingDocument>> {
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
if (isParsed && parsedDoc != null) {
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.success, document: parsedDoc));
|
||||
if (resultDoc != null) {
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.success, document: resultDoc));
|
||||
} else {
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.error, errorMessage: 'Gagal mengekstrak data (Timeout).'));
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.error, errorMessage: failureMessage ?? 'Gagal mengekstrak data (Timeout).'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +187,11 @@ class PendingDocumentsNotifier extends StateNotifier<List<PendingDocument>> {
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.uploading, errorMessage: null));
|
||||
_uploadAndProcess(id, doc.imagePath, latitude: doc.latitude, longitude: doc.longitude);
|
||||
}
|
||||
|
||||
void clearQueue() {
|
||||
state = const [];
|
||||
_ref.read(localStorageProvider).pendingDocumentsBox.clear();
|
||||
}
|
||||
}
|
||||
|
||||
final pendingDocumentsProvider = StateNotifierProvider<PendingDocumentsNotifier, List<PendingDocument>>((ref) {
|
||||
|
||||
Reference in new issue
Block a user