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:
1 parent
095dd4cb8b
commit
4808a798fb
78 files changed
+8069
-584
No files matched your search
@@ -158,10 +158,10 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Confirm Submission', style: AppConfig.h2),
|
||||
Text('Konfirmasi Pengiriman', style: AppConfig.h2),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Please enter your name (as recipient/storekeeper) and check the agreement box to confirm delivery.',
|
||||
'Masukkan nama Anda (sebagai penerima/penjaga toko) dan centang kotak persetujuan untuk mengonfirmasi pengiriman.',
|
||||
style: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -195,7 +195,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text('I agree and confirm the data is accurate.', style: AppConfig.bodyMedium),
|
||||
child: Text('Saya setuju dan mengonfirmasi bahwa data ini sudah benar.', style: AppConfig.bodyMedium),
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -208,7 +208,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
Navigator.pop(context); // close modal
|
||||
_submitDocument();
|
||||
} : null,
|
||||
child: const Text('Confirm'),
|
||||
child: const Text('Konfirmasi'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -259,45 +259,46 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
await localStorage.saveDocument(finalDoc);
|
||||
|
||||
// Try to sync with server
|
||||
bool syncedToServer = false;
|
||||
String? syncError;
|
||||
try {
|
||||
final apiClient = ApiClient();
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
await apiClient.client.put(
|
||||
'${AppConfig.fetchDocumentsEndpoint}/${finalDoc.id}',
|
||||
data: {
|
||||
'tanggal': finalDoc.tanggal,
|
||||
'noPo': finalDoc.noPo,
|
||||
'noSo': finalDoc.noSo,
|
||||
'noDo': finalDoc.noDo,
|
||||
'kepadaYth': finalDoc.kepadaYth,
|
||||
'orderUntuk': finalDoc.orderUntuk,
|
||||
'alamat': finalDoc.alamat,
|
||||
'platTruk': finalDoc.platTruk,
|
||||
'namaDriver': finalDoc.namaDriver,
|
||||
'namaPenerima': finalDoc.namaPenerima,
|
||||
'latitude': finalDoc.latitude,
|
||||
'longitude': finalDoc.longitude,
|
||||
'items': finalDoc.items.map((e) => {
|
||||
'nomor_sku': e.nomorSku,
|
||||
'nama_barang': e.namaBarang,
|
||||
'banyak': e.banyak,
|
||||
'jumlah': e.jumlah,
|
||||
}).toList(),
|
||||
},
|
||||
data: finalDoc.toPutPayload(),
|
||||
);
|
||||
syncedToServer = true;
|
||||
debugPrint('Document synced with server successfully');
|
||||
} catch (e) {
|
||||
syncError = e.toString();
|
||||
debugPrint('Failed to sync document with server: $e. Saved locally (cached).');
|
||||
}
|
||||
|
||||
// Remove from pending
|
||||
if (widget.pendingId != null) {
|
||||
ref.read(pendingDocumentsProvider.notifier).removeDocument(widget.pendingId!);
|
||||
if (syncedToServer) {
|
||||
ref.read(pendingDocumentsProvider.notifier).removeDocument(widget.pendingId!);
|
||||
} else {
|
||||
// Keep it visible under "Pending & Processing" with a distinct status so the
|
||||
// correction isn't silently lost - the user can retry the sync from there
|
||||
// without re-uploading the image or re-running OCR.
|
||||
ref.read(pendingDocumentsProvider.notifier).markSyncFailed(
|
||||
widget.pendingId!,
|
||||
finalDoc,
|
||||
'Belum tersinkron ke server: $syncError',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// Show SnackBar BEFORE pop so context is still valid
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Document Saved Successfully!'), backgroundColor: AppConfig.successColor),
|
||||
syncedToServer
|
||||
? const SnackBar(content: Text('Dokumen Berhasil Disimpan!'), backgroundColor: AppConfig.successColor)
|
||||
: const SnackBar(
|
||||
content: Text('Tersimpan lokal, tapi GAGAL sinkron ke server. Cek koneksi lalu retry dari daftar dokumen.'),
|
||||
backgroundColor: Colors.deepOrange,
|
||||
duration: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
// Pass true as result so DocumentsScreen's .then() knows to reload
|
||||
context.pop(true);
|
||||
@@ -342,7 +343,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
),
|
||||
validator: validator ?? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter $label';
|
||||
return '$label harus diisi';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -354,14 +355,14 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
if (_document == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Review Data')),
|
||||
body: const Center(child: Text('Document not found or still processing')),
|
||||
appBar: AppBar(title: const Text('Tinjau Data')),
|
||||
body: const Center(child: Text('Dokumen tidak ditemukan atau masih diproses')),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Review & Edit Data'),
|
||||
title: const Text('Tinjau & Edit Data'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.pop(),
|
||||
@@ -387,7 +388,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Please review and edit the extracted data below to ensure accuracy before confirmation.',
|
||||
'Tinjau dan edit data hasil ekstraksi di bawah ini untuk memastikan keakuratannya sebelum konfirmasi.',
|
||||
style: AppConfig.bodyMedium.copyWith(color: AppConfig.primaryColor),
|
||||
),
|
||||
),
|
||||
@@ -408,7 +409,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Header Details', style: AppConfig.h2),
|
||||
Text('Detail Header', style: AppConfig.h2),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Tanggal',
|
||||
@@ -443,7 +444,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
'No PO',
|
||||
_noPoCtrl,
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) return 'Please enter No PO';
|
||||
if (val == null || val.isEmpty) return 'No PO harus diisi';
|
||||
if (!RegExp(r'^PO/\d{2}/\d{10}$').hasMatch(val)) {
|
||||
return 'Format harus PO/[XX]/[XXXXXXXXXX]';
|
||||
}
|
||||
@@ -459,7 +460,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) return 'Please enter No SO';
|
||||
if (val == null || val.isEmpty) return 'No SO harus diisi';
|
||||
if (val.length != 10) {
|
||||
return 'SO harus persis 10 angka';
|
||||
}
|
||||
@@ -485,7 +486,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Shipment Details', style: AppConfig.h2),
|
||||
Text('Detail Pengiriman', style: AppConfig.h2),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField('Kepada Yth', _kepadaYthCtrl),
|
||||
_buildTextField('Order Untuk', _orderUntukCtrl),
|
||||
@@ -510,15 +511,15 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Geotagging Details', style: AppConfig.h2),
|
||||
Text('Detail Lokasi', style: AppConfig.h2),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Latitude',
|
||||
'Lintang',
|
||||
_latitudeCtrl,
|
||||
readOnly: true,
|
||||
),
|
||||
_buildTextField(
|
||||
'Longitude',
|
||||
'Bujur',
|
||||
_longitudeCtrl,
|
||||
readOnly: true,
|
||||
),
|
||||
@@ -543,7 +544,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Items (${_itemControllers.length})', style: AppConfig.h2),
|
||||
Text('Barang (${_itemControllers.length})', style: AppConfig.h2),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -581,7 +582,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
onPressed: () => _removeItem(index),
|
||||
tooltip: 'Delete Item',
|
||||
tooltip: 'Hapus Barang',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -591,7 +592,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
ctrls['sku']!,
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please enter SKU';
|
||||
return 'SKU harus diisi';
|
||||
}
|
||||
if (!MasterSku.data.containsKey(val)) {
|
||||
return 'SKU tidak terdaftar';
|
||||
@@ -647,7 +648,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: _showConfirmationDialog,
|
||||
child: const Text('Review Complete', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
child: const Text('Selesai Ditinjau', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -23,7 +23,7 @@ class PdfService {
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Text('RECEIPT OF DELIVERY', style: pw.TextStyle(fontSize: 24, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text('TANDA TERIMA PENGIRIMAN', style: pw.TextStyle(fontSize: 24, fontWeight: pw.FontWeight.bold)),
|
||||
pw.SizedBox(
|
||||
width: 60,
|
||||
height: 60,
|
||||
@@ -53,7 +53,7 @@ class PdfService {
|
||||
pw.SizedBox(height: 20),
|
||||
|
||||
// Shipment Data
|
||||
pw.Text('Shipment Details', style: pw.TextStyle(fontSize: 16, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text('Detail Pengiriman', style: pw.TextStyle(fontSize: 16, fontWeight: pw.FontWeight.bold)),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Container(
|
||||
padding: const pw.EdgeInsets.all(10),
|
||||
@@ -75,7 +75,7 @@ class PdfService {
|
||||
pw.SizedBox(height: 20),
|
||||
|
||||
// Items Table
|
||||
pw.Text('Items Delivered', style: pw.TextStyle(fontSize: 16, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text('Barang Dikirim', style: pw.TextStyle(fontSize: 16, fontWeight: pw.FontWeight.bold)),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Table.fromTextArray(
|
||||
context: context,
|
||||
|
||||
Reference in new issue
Block a user