part of 'editor_screen.dart'; mixin EditorLogic on ConsumerState { DocumentModel? _document; // Controllers final _tanggalCtrl = TextEditingController(); final _noPoCtrl = TextEditingController(); final _noSoCtrl = TextEditingController(); final _noDoCtrl = TextEditingController(); final _kepadaYthCtrl = TextEditingController(); final _orderUntukCtrl = TextEditingController(); final _alamatCtrl = TextEditingController(); final _platTrukCtrl = TextEditingController(); final _namaDriverCtrl = TextEditingController(); final _namaPenerimaCtrl = TextEditingController(); final _latitudeCtrl = TextEditingController(); final _longitudeCtrl = TextEditingController(); List> _itemControllers = []; bool _isAgreed = false; final _formKey = GlobalKey(); 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); 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 ConfirmationDialog( nameController: _namaPenerimaCtrl, initialAgreed: _isAgreed, onConfirm: (agreed, name) { setState(() { _isAgreed = agreed; }); Navigator.pop(context); _submitDocument(); }, ); } ); } Future _submitDocument() async { 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 saveAction = resolveDocumentSaveAction( existingDocument: _document, localImagePath: pendingDoc.imagePath, ); if (saveAction.kind == DocumentSaveActionKind.blocked) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(saveAction.blockedMessage!), backgroundColor: AppConfig.errorColor, duration: const Duration(seconds: 5), ), ); return; } final apiClient = ref.read(apiClientProvider); String serverId; if (saveAction.kind == DocumentSaveActionKind.reuploadThenPut) { try { final formData = FormData.fromMap({ 'image': await MultipartFile.fromFile( pendingDoc.imagePath, filename: pendingDoc.imagePath.split(RegExp(r'[/\\]')).last, ), if (finalLat != null) 'latitude': finalLat.toString(), if (finalLng != null) 'longitude': finalLng.toString(), 'scan_mode': pendingDoc.scanMode ?? 'DO', }); final response = await apiClient.client.post(AppConfig.uploadDocumentEndpoint, data: formData); serverId = DocumentModel.fromJson(response.data['data']).id; } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal membuat dokumen di server: ${ApiException.from(e).displayMessage}'), backgroundColor: AppConfig.errorColor, duration: const Duration(seconds: 5), ), ); return; } } else { serverId = saveAction.serverId!; } final finalDoc = DocumentModel( id: serverId, 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(), ); final localStorage = ref.read(localStorageProvider); await localStorage.saveDocument(finalDoc); bool syncedToServer = false; String? syncError; try { await apiClient.client.put( '${AppConfig.fetchDocumentsEndpoint}/${finalDoc.id}', data: finalDoc.toPutPayload(), ); syncedToServer = true; } catch (e) { syncError = ApiException.from(e).displayMessage; } if (widget.pendingId != null) { if (syncedToServer) { ref.read(pendingDocumentsProvider.notifier).removeDocument(widget.pendingId!); } else { ref.read(pendingDocumentsProvider.notifier).markSyncFailed( widget.pendingId!, finalDoc, 'Belum tersinkron ke server: $syncError', ); } } if (mounted) { 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), ), ); context.pop(true); } } }