import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../config/app_config.dart'; import '../../models/document_model.dart'; import '../../main.dart'; // To access localStorageProvider import '../documents/pending_documents_provider.dart'; import '../../data/master_sku.dart'; import '../../core/network/api_client.dart'; class EditorScreen extends ConsumerStatefulWidget { final String? pendingId; const EditorScreen({super.key, this.pendingId}); @override ConsumerState createState() => _EditorScreenState(); } class _EditorScreenState extends ConsumerState { DocumentModel? _document; // Controllers for Header final _tanggalCtrl = TextEditingController(); final _noPoCtrl = TextEditingController(); final _noSoCtrl = TextEditingController(); final _noDoCtrl = TextEditingController(); // Controllers for Shipment final _kepadaYthCtrl = TextEditingController(); final _orderUntukCtrl = TextEditingController(); final _alamatCtrl = TextEditingController(); final _platTrukCtrl = TextEditingController(); final _namaDriverCtrl = TextEditingController(); final _namaPenerimaCtrl = TextEditingController(); // Controllers for Geotagging final _latitudeCtrl = TextEditingController(); final _longitudeCtrl = TextEditingController(); // Controllers for Items List> _itemControllers = []; bool _isAgreed = false; final _formKey = GlobalKey(); @override void initState() { super.initState(); _loadDocumentData(); } void _loadDocumentData() { if (widget.pendingId == null) { return; } final pendingDocs = ref.read(pendingDocumentsProvider); final pendingDoc = pendingDocs.firstWhere( (doc) => doc.id == widget.pendingId, orElse: () => PendingDocument(id: '', imagePath: '') ); if (pendingDoc.document != null) { final doc = pendingDoc.document!; setState(() { _document = doc; _tanggalCtrl.text = doc.tanggal; _noPoCtrl.text = doc.noPo; _noSoCtrl.text = doc.noSo; _noDoCtrl.text = doc.noDo; _kepadaYthCtrl.text = doc.kepadaYth; _orderUntukCtrl.text = doc.orderUntuk; _alamatCtrl.text = doc.alamat; _platTrukCtrl.text = doc.platTruk; _namaDriverCtrl.text = doc.namaDriver; final lat = pendingDoc.latitude ?? doc.latitude; final lng = pendingDoc.longitude ?? doc.longitude; _latitudeCtrl.text = lat != null ? lat.toString() : '-'; _longitudeCtrl.text = lng != null ? lng.toString() : '-'; for (var item in doc.items) { _addItem( sku: item.nomorSku, nama: item.namaBarang, banyak: item.banyak, jumlah: item.jumlah, ); } }); } } void _addItem({String sku = '', String nama = '', String banyak = '', String jumlah = ''}) { final skuCtrl = TextEditingController(text: sku); final namaCtrl = TextEditingController(text: nama); final banyakCtrl = TextEditingController(text: banyak); final jumlahCtrl = TextEditingController(text: jumlah); // Auto rename based on master SKU skuCtrl.addListener(() { final currentSku = skuCtrl.text; if (MasterSku.data.containsKey(currentSku)) { namaCtrl.text = MasterSku.data[currentSku]!; } else { namaCtrl.text = 'SKU Tidak Terdaftar'; } }); setState(() { _itemControllers.add({ 'sku': skuCtrl, 'nama': namaCtrl, 'banyak': banyakCtrl, 'jumlah': jumlahCtrl, }); }); } void _removeItem(int index) { setState(() { final ctrls = _itemControllers[index]; ctrls['sku']?.dispose(); ctrls['nama']?.dispose(); ctrls['banyak']?.dispose(); ctrls['jumlah']?.dispose(); _itemControllers.removeAt(index); }); } void _showConfirmationDialog() { if (!_formKey.currentState!.validate()) return; showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setModalState) { final isButtonEnabled = _isAgreed && _namaPenerimaCtrl.text.trim().isNotEmpty; return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, left: 24, right: 24, top: 24, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Konfirmasi Pengiriman', style: AppConfig.h2), const SizedBox(height: 16), Text( '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), TextField( controller: _namaPenerimaCtrl, onChanged: (val) { setModalState(() {}); }, style: AppConfig.bodyMedium, decoration: InputDecoration( labelText: 'Nama Penerima (Penjaga Toko)', labelStyle: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary), filled: true, fillColor: Colors.grey.shade50, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), ), ), const SizedBox(height: 16), Row( children: [ Checkbox( value: _isAgreed, activeColor: AppConfig.primaryColor, onChanged: (val) { setModalState(() { _isAgreed = val ?? false; }); }, ), Expanded( child: Text('Saya setuju dan mengonfirmasi bahwa data ini sudah benar.', style: AppConfig.bodyMedium), ) ], ), const SizedBox(height: 24), SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: isButtonEnabled ? () { Navigator.pop(context); // close modal _submitDocument(); } : null, child: const Text('Konfirmasi'), ), ), const SizedBox(height: 24), ], ), ); } ); } ); } Future _submitDocument() async { // Construct final document final pendingDocs = ref.read(pendingDocumentsProvider); final pendingDoc = pendingDocs.firstWhere( (doc) => doc.id == widget.pendingId, orElse: () => PendingDocument(id: '', imagePath: '') ); final double? finalLat = pendingDoc.latitude ?? _document?.latitude; final double? finalLng = pendingDoc.longitude ?? _document?.longitude; final finalDoc = DocumentModel( id: _document?.id ?? DateTime.now().millisecondsSinceEpoch.toString(), tanggal: _tanggalCtrl.text, noPo: _noPoCtrl.text, noSo: _noSoCtrl.text, noDo: _noDoCtrl.text, kepadaYth: _kepadaYthCtrl.text, orderUntuk: _orderUntukCtrl.text, alamat: _alamatCtrl.text, platTruk: _platTrukCtrl.text, namaDriver: _namaDriverCtrl.text, namaPenerima: _namaPenerimaCtrl.text, latitude: finalLat, longitude: finalLng, items: _itemControllers.map((ctrls) => DocumentItem( nomorSku: ctrls['sku']!.text, namaBarang: ctrls['nama']!.text, banyak: ctrls['banyak']!.text, jumlah: ctrls['jumlah']!.text, )).toList(), createdAt: DateTime.now(), ); // Save locally via Riverpod injected LocalStorage final localStorage = ref.read(localStorageProvider); await localStorage.saveDocument(finalDoc); // Try to sync with server bool syncedToServer = false; String? syncError; try { final apiClient = ref.read(apiClientProvider); await apiClient.client.put( '${AppConfig.fetchDocumentsEndpoint}/${finalDoc.id}', 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).'); } if (widget.pendingId != null) { 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( 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); } } Widget _buildTextField( String label, TextEditingController controller, { TextInputType keyboardType = TextInputType.text, bool readOnly = false, VoidCallback? onTap, List? inputFormatters, String? Function(String?)? validator, }) { return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: TextFormField( controller: controller, keyboardType: keyboardType, readOnly: readOnly, onTap: onTap, inputFormatters: inputFormatters, style: AppConfig.bodyMedium, decoration: InputDecoration( labelText: label, labelStyle: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary), filled: true, fillColor: Colors.grey.shade50, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: AppConfig.primaryColor), ), ), validator: validator ?? (value) { if (value == null || value.isEmpty) { return '$label harus diisi'; } return null; }, ), ); } @override Widget build(BuildContext context) { if (_document == null) { return Scaffold( 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('Tinjau & Edit Data'), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.pop(), ), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppConfig.primaryColor.withOpacity(0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppConfig.primaryColor.withOpacity(0.2)), ), child: Row( children: [ const Icon(Icons.info_outline, color: AppConfig.primaryColor), const SizedBox(width: 12), Expanded( child: Text( 'Tinjau dan edit data hasil ekstraksi di bawah ini untuk memastikan keakuratannya sebelum konfirmasi.', style: AppConfig.bodyMedium.copyWith(color: AppConfig.primaryColor), ), ), ], ), ), const SizedBox(height: 24), // Header Details Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Detail Header', style: AppConfig.h2), const SizedBox(height: 16), _buildTextField( 'Tanggal', _tanggalCtrl, readOnly: true, onTap: () async { DateTime? pickedDate = await showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime(2000), lastDate: DateTime(2101), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light( primary: AppConfig.primaryColor, onPrimary: Colors.white, onSurface: AppConfig.textPrimary, ), ), child: child!, ); }, ); if (pickedDate != null) { String formattedDate = DateFormat('dd MMMM yyyy').format(pickedDate); _tanggalCtrl.text = formattedDate; } } ), _buildTextField( 'No PO', _noPoCtrl, validator: (val) { 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]'; } return null; } ), _buildTextField( 'No SO', _noSoCtrl, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), ], validator: (val) { if (val == null || val.isEmpty) return 'No SO harus diisi'; if (val.length != 10) { return 'SO harus persis 10 angka'; } return null; } ), _buildTextField('No DO', _noDoCtrl), ], ), ), ), const SizedBox(height: 16), // Shipment Details Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Detail Pengiriman', style: AppConfig.h2), const SizedBox(height: 16), _buildTextField('Kepada Yth', _kepadaYthCtrl), _buildTextField('Order Untuk', _orderUntukCtrl), _buildTextField('Alamat', _alamatCtrl), _buildTextField('Plat Truk', _platTrukCtrl), _buildTextField('Nama Driver', _namaDriverCtrl), ], ), ), ), const SizedBox(height: 16), // Geotagging Details Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Detail Lokasi', style: AppConfig.h2), const SizedBox(height: 16), _buildTextField( 'Lintang', _latitudeCtrl, readOnly: true, ), _buildTextField( 'Bujur', _longitudeCtrl, readOnly: true, ), ], ), ), ), const SizedBox(height: 16), // Items Section (Editable + CRUD) Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Barang (${_itemControllers.length})', style: AppConfig.h2), ], ), const SizedBox(height: 16), ..._itemControllers.asMap().entries.map((entry) { int index = entry.key; var ctrls = entry.value; return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: ValueListenableBuilder( valueListenable: ctrls['nama']!, builder: (context, value, child) { return Text( value.text.isNotEmpty ? value.text : 'Item Baru', style: AppConfig.h3, maxLines: 1, overflow: TextOverflow.ellipsis, ); }, ), ), IconButton( icon: const Icon(Icons.delete_outline, color: Colors.red), onPressed: () => _removeItem(index), tooltip: 'Hapus Barang', ), ], ), const SizedBox(height: 8), _buildTextField( 'SKU', ctrls['sku']!, validator: (val) { if (val == null || val.isEmpty) { return 'SKU harus diisi'; } if (!MasterSku.data.containsKey(val)) { return 'SKU tidak terdaftar'; } return null; }, ), _buildTextField('Nama Barang', ctrls['nama']!), Row( children: [ Expanded(child: _buildTextField('Banyak', ctrls['banyak']!)), const SizedBox(width: 8), Expanded(child: _buildTextField('Jumlah', ctrls['jumlah']!)), ], ), ], ), ), ); }), const SizedBox(height: 8), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () => _addItem(), icon: const Icon(Icons.add), label: const Text('Tambah Item Baru'), style: OutlinedButton.styleFrom( foregroundColor: AppConfig.primaryColor, side: const BorderSide(color: AppConfig.primaryColor), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(vertical: 12), ), ), ), ], ), ), ), const SizedBox(height: 32), ], ), ), ), bottomNavigationBar: SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), child: SizedBox( height: 54, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: _showConfirmationDialog, child: const Text('Selesai Ditinjau', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ), ), ), ), ); } @override void dispose() { _tanggalCtrl.dispose(); _noPoCtrl.dispose(); _noSoCtrl.dispose(); _noDoCtrl.dispose(); _kepadaYthCtrl.dispose(); _orderUntukCtrl.dispose(); _alamatCtrl.dispose(); _platTrukCtrl.dispose(); _namaDriverCtrl.dispose(); _namaPenerimaCtrl.dispose(); _latitudeCtrl.dispose(); _longitudeCtrl.dispose(); for (var ctrls in _itemControllers) { ctrls['sku']?.dispose(); ctrls['nama']?.dispose(); ctrls['banyak']?.dispose(); ctrls['jumlah']?.dispose(); } super.dispose(); } }