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

+47 -12
View File
@@ -37,7 +37,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
});
try {
final apiClient = ApiClient();
final apiClient = ref.read(apiClientProvider);
final response = await apiClient.client.get(AppConfig.fetchDocumentsEndpoint);
final List serverData = response.data['data'] ?? [];
@@ -88,7 +88,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
return Scaffold(
appBar: AppBar(
title: const Text('Documents'),
title: const Text('Dokumen'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/camera'),
@@ -103,7 +103,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
child: TextField(
controller: _searchController,
decoration: const InputDecoration(
hintText: 'Search by No PO, No SO, DO, Date...',
hintText: 'Cari berdasarkan No PO, No SO, DO, Tanggal...',
prefixIcon: Icon(Icons.search),
),
),
@@ -118,7 +118,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
children: [
Icon(Icons.folder_open, size: 64, color: AppConfig.textSecondary.withOpacity(0.5)),
const SizedBox(height: 16),
Text('No documents found', style: AppConfig.bodyLarge),
Text('Tidak ada dokumen ditemukan', style: AppConfig.bodyLarge),
],
),
)
@@ -128,7 +128,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
if (pendingDocs.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text('Pending & Processing', style: AppConfig.h3),
child: Text('Tertunda & Diproses', style: AppConfig.h3),
),
...pendingDocs.map((doc) => _buildPendingCard(doc)),
const SizedBox(height: 16),
@@ -138,7 +138,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
if (_filteredDocuments.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text('Confirmed Documents', style: AppConfig.h3),
child: Text('Dokumen Terkonfirmasi', style: AppConfig.h3),
),
..._filteredDocuments.map((doc) => _buildDocumentCard(doc)),
],
@@ -172,6 +172,11 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
statusText = 'Gagal Memproses';
statusIcon = Icons.error;
break;
case PendingDocumentStatus.syncFailed:
statusColor = Colors.deepOrange;
statusText = 'Belum tersinkron ke server';
statusIcon = Icons.sync_problem;
break;
}
return Card(
@@ -198,7 +203,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
children: [
ListTile(
leading: const Icon(Icons.refresh),
title: const Text('Retry Upload'),
title: const Text('Coba Unggah Ulang'),
onTap: () {
Navigator.pop(ctx);
ref.read(pendingDocumentsProvider.notifier).retryUpload(doc.id);
@@ -206,7 +211,35 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
),
ListTile(
leading: const Icon(Icons.delete, color: AppConfig.errorColor),
title: const Text('Delete Document', style: TextStyle(color: AppConfig.errorColor)),
title: const Text('Hapus Dokumen', style: TextStyle(color: AppConfig.errorColor)),
onTap: () {
Navigator.pop(ctx);
ref.read(pendingDocumentsProvider.notifier).removeDocument(doc.id);
},
),
],
),
),
);
} else if (doc.status == PendingDocumentStatus.syncFailed) {
// The correction was saved locally but never reached the server - retry
// just the sync (no re-upload, no re-running OCR).
showModalBottomSheet(
context: context,
builder: (ctx) => SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(Icons.sync),
title: const Text('Coba Sinkron Ulang'),
onTap: () {
Navigator.pop(ctx);
ref.read(pendingDocumentsProvider.notifier).retrySync(doc.id);
},
),
ListTile(
leading: const Icon(Icons.delete, color: AppConfig.errorColor),
title: const Text('Hapus Dokumen', style: TextStyle(color: AppConfig.errorColor)),
onTap: () {
Navigator.pop(ctx);
ref.read(pendingDocumentsProvider.notifier).removeDocument(doc.id);
@@ -263,7 +296,9 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
if (doc.status == PendingDocumentStatus.success)
Text('Ketuk untuk mereview dan konfirmasi data', style: AppConfig.bodySmall),
if (doc.status == PendingDocumentStatus.error)
Text(doc.errorMessage ?? 'Unknown error', style: AppConfig.bodySmall.copyWith(color: AppConfig.errorColor), maxLines: 1, overflow: TextOverflow.ellipsis),
Text(doc.errorMessage ?? 'Kesalahan tidak diketahui', style: AppConfig.bodySmall.copyWith(color: AppConfig.errorColor), maxLines: 1, overflow: TextOverflow.ellipsis),
if (doc.status == PendingDocumentStatus.syncFailed)
Text(doc.errorMessage ?? 'Ketuk untuk coba sinkron ulang', style: AppConfig.bodySmall.copyWith(color: Colors.deepOrange), maxLines: 1, overflow: TextOverflow.ellipsis),
],
),
),
@@ -312,7 +347,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
children: [
const Icon(Icons.check_circle, size: 14, color: AppConfig.successColor),
const SizedBox(width: 4),
Text('Confirmed', style: AppConfig.bodySmall.copyWith(color: AppConfig.successColor, fontWeight: FontWeight.bold)),
Text('Terkonfirmasi', style: AppConfig.bodySmall.copyWith(color: AppConfig.successColor, fontWeight: FontWeight.bold)),
],
),
),
@@ -362,7 +397,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
child: OutlinedButton.icon(
onPressed: () => _printReceipt(doc),
icon: const Icon(Icons.print, color: AppConfig.primaryColor),
label: const Text('Print Receipt', style: TextStyle(color: AppConfig.primaryColor, fontWeight: FontWeight.bold)),
label: const Text('Cetak Tanda Terima', style: TextStyle(color: AppConfig.primaryColor, fontWeight: FontWeight.bold)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: AppConfig.primaryColor),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
@@ -397,7 +432,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
Future<void> _printReceipt(DocumentModel doc) async {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Generating PDF...'),
content: Text('Membuat PDF...'),
duration: Duration(seconds: 1),
backgroundColor: AppConfig.secondaryColor,
),