Add mobile reliability fixes, Bahasa Indonesia UI, and continue OCR accuracy tuning

Reliability/PoC hardening: dedupe uploads by file_hash, surface editor sync
failures instead of a false success SnackBar with a retry-without-re-OCR path,
bound the OCR pipeline fetches with timeouts, share a single ApiClient/Dio
instance app-wide, tune capture JPEG quality, and add an opt-in
docker-compose.demo.yml for a production-mode run ahead of client demos.

Translate all Flutter-side user-facing text (screens, validators, SnackBars,
the printed delivery receipt, and shared API error messages) to Bahasa
Indonesia.

Also includes in-progress OCR parser/accuracy-tuning work from the same
session: table column/unit normalization fixes, store/customer master data,
accuracy history log, and test-image renaming/cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eRAsLqN9Sg1b9YPz22Lxz
This commit is contained in:
Rafhan Mazaya FathurrahmanandClaude Sonnet 5 committed 2026-07-04 19:41:24 +07:00
1 parent 095dd4cb8b
commit 4808a798fb
78 files changed
+8069 -584

No files matched your search

@@ -6,7 +6,7 @@ import '../../core/location/location_service.dart';
import '../../core/network/api_client.dart';
import '../../config/app_config.dart';
enum PendingDocumentStatus { uploading, processing, success, error }
enum PendingDocumentStatus { uploading, processing, success, error, syncFailed }
class PendingDocument {
final String id;
@@ -50,7 +50,9 @@ class PendingDocument {
}
class PendingDocumentsNotifier extends StateNotifier<List<PendingDocument>> {
PendingDocumentsNotifier() : super([]);
PendingDocumentsNotifier(this._ref) : super([]);
final Ref _ref;
void addDocument(String imagePath, {double? latitude, double? longitude}) {
final id = DateTime.now().millisecondsSinceEpoch.toString();
@@ -96,7 +98,7 @@ class PendingDocumentsNotifier extends StateNotifier<List<PendingDocument>> {
try {
// API call to upload and process
final apiClient = ApiClient();
final apiClient = _ref.read(apiClientProvider);
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(
imagePath,
@@ -192,7 +194,56 @@ class PendingDocumentsNotifier extends StateNotifier<List<PendingDocument>> {
void removeDocument(String id) {
state = state.where((item) => item.id != id).toList();
}
/// Marks a pending item as failed to sync after an editor save, keeping the
/// finalized [doc] so [retrySync] can resend it without re-running OCR.
void markSyncFailed(String id, DocumentModel doc, String message) {
state = [
for (final item in state)
if (item.id == id)
item.copyWith(status: PendingDocumentStatus.syncFailed, document: doc, errorMessage: message)
else
item
];
}
/// Resends a previously-failed editor save. Unlike [retryUpload], this never
/// re-uploads the image or re-runs OCR - it only retries the PUT with the
/// already-corrected [PendingDocument.document].
Future<void> retrySync(String id) async {
final matches = state.where((item) => item.id == id);
if (matches.isEmpty) return;
final doc = matches.first.document;
if (doc == null) return;
state = [
for (final item in state)
if (item.id == id)
item.copyWith(status: PendingDocumentStatus.uploading, errorMessage: null)
else
item
];
try {
final apiClient = _ref.read(apiClientProvider);
await apiClient.client.put(
'${AppConfig.fetchDocumentsEndpoint}/${doc.id}',
data: doc.toPutPayload(),
);
if (!mounted) return;
removeDocument(id);
} catch (e) {
if (!mounted) return;
state = [
for (final item in state)
if (item.id == id)
item.copyWith(status: PendingDocumentStatus.syncFailed, errorMessage: 'Gagal sinkronisasi ke server: ${e.toString()}')
else
item
];
}
}
void retryUpload(String id) {
final doc = state.firstWhere((item) => item.id == id);
final lat = doc.latitude;
@@ -212,5 +263,5 @@ class PendingDocumentsNotifier extends StateNotifier<List<PendingDocument>> {
}
final pendingDocumentsProvider = StateNotifierProvider<PendingDocumentsNotifier, List<PendingDocument>>((ref) {
return PendingDocumentsNotifier();
return PendingDocumentsNotifier(ref);
});