import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:get/get.dart'; import 'package:praresi/presentation/controllers/internet_controller.dart'; class ResiController extends GetxController { final FirebaseFirestore _firestore = FirebaseFirestore.instance; List> _wilayahData = []; var isSaving = false.obs; // ✅ loading save @override void onInit() { super.onInit(); _loadWilayahData(); } /// 🔹 Muat data provinsi & kota dari JSON lokal Future _loadWilayahData() async { try { final String jsonString = await rootBundle.loadString('assets/data/regions.json'); final List jsonData = jsonDecode(jsonString); _wilayahData = jsonData.cast>(); } catch (e) { debugPrint("Gagal memuat data wilayah: $e"); } } /// 🔹 Deteksi provinsi & kota dari alamat (prioritaskan akhir alamat + fuzzy + aman) // Map _extractWilayah(String alamat) { // if (alamat.isEmpty || _wilayahData.isEmpty) { // return {'kota': null, 'provinsi': null}; // } // String alamatLower = alamat.toLowerCase(); // String? provinsi; // String? kota; // double bestScore = 0.0; // // Ambil 4 kata terakhir, karena umumnya di situ ada kota/provinsi // List alamatWords = alamatLower.split(RegExp(r'\s+')); // String lastPart = alamatWords.length > 4 // ? alamatWords.sublist(alamatWords.length - 4).join(' ') // : alamatLower; // for (var data in _wilayahData) { // if (data['provinsi'] == null || data['kota'] == null) continue; // final String prov = data['provinsi'].toString(); // final List kotaList = List.from(data['kota']); // final String provLower = prov.toLowerCase(); // // 🔹 Deteksi provinsi langsung atau pakai fuzzy matching // if (lastPart.contains(provLower)) { // provinsi = prov; // } else if (_similarity(lastPart, provLower) > 0.7) { // provinsi = prov; // } else { // // Cek singkatan umum provinsi // if (provLower.contains('jawa timur') && lastPart.contains('jatim')) provinsi = prov; // if (provLower.contains('jawa tengah') && lastPart.contains('jateng')) provinsi = prov; // if (provLower.contains('jawa barat') && lastPart.contains('jabar')) provinsi = prov; // if (provLower.contains('daerah istimewa yogyakarta') && lastPart.contains('diy')) provinsi = prov; // } // // 🔹 Deteksi kota // for (var k in kotaList) { // if (k == null) continue; // final String kotaNama = k.toString(); // final String kotaLower = kotaNama.toLowerCase(); // final String kotaBersih = kotaLower.replaceAll(RegExp(r'^(kab\.?|kota)\s*'), '').trim(); // // Hindari error: pastikan index valid // int indexKota = alamatLower.contains(kotaBersih) // ? alamatLower.lastIndexOf(kotaBersih) // : -1; // // Semakin ke akhir alamat, bobot lebih tinggi // double weight = (indexKota > 0) // ? (indexKota / alamatLower.length).clamp(0.0, 1.0) // : 0.0; // // Skor kemiripan // double score; // if (alamatLower.contains(kotaBersih)) { // score = 0.9 + weight * 0.1; // } else { // score = _similarity(lastPart, kotaBersih) + weight * 0.1; // } // // Simpan hasil terbaik // if (score > bestScore) { // bestScore = score; // kota = kotaNama; // provinsi ??= prov; // } // } // } // return { // 'kota': kota, // 'provinsi': provinsi, // }; // } Map _extractWilayah(String alamat) { if (alamat.isEmpty || _wilayahData.isEmpty) { return {'kota': null, 'provinsi': null}; } String alamatLower = alamat.toLowerCase(); String? provinsi; String? kota; // 🔹 Ambil bagian belakang alamat (lebih akurat) List words = alamatLower.split(RegExp(r'\s+')); String lastPart = words.length > 5 ? words.sublist(words.length - 5).join(' ') : alamatLower; // ============================== // 🔹 STEP 1: DETEKSI PROVINSI DULU // ============================== for (var data in _wilayahData) { final prov = data['provinsi']?.toString() ?? ''; final provLower = prov.toLowerCase(); if (provLower.isEmpty) continue; if (lastPart.contains(provLower) || _similarity(lastPart, provLower) > 0.7 || (provLower.contains('jawa timur') && lastPart.contains('jatim')) || (provLower.contains('jawa tengah') && lastPart.contains('jateng')) || (provLower.contains('jawa barat') && lastPart.contains('jabar')) || (provLower.contains('yogyakarta') && lastPart.contains('diy'))) { provinsi = prov; break; // 🔥 STOP kalau sudah ketemu } } // ❌ kalau provinsi tidak ketemu → langsung return if (provinsi == null) { return {'kota': null, 'provinsi': null}; } // ============================== // 🔹 STEP 2: CARI KOTA DALAM PROVINSI TERSEBUT // ============================== final provData = _wilayahData.firstWhere( (e) => e['provinsi'] == provinsi, orElse: () => {}, ); final List kotaList = List.from(provData['kota'] ?? []); double bestScore = 0; for (var k in kotaList) { final kotaNama = k.toString(); final kotaLower = kotaNama.toLowerCase(); final kotaBersih = kotaLower.replaceAll(RegExp(r'^(kab\.?|kota)\s*'), '').trim(); double score; if (alamatLower.contains(kotaBersih)) { score = 0.95; } else { score = _similarity(lastPart, kotaBersih); } if (score > bestScore) { bestScore = score; kota = kotaNama; } } // 🔥 Tambahan penting di sini if (bestScore < 0.75) { kota = null; } return { 'kota': kota, 'provinsi': provinsi, }; } /// 🔹 Fungsi bantu: fuzzy similarity sederhana antar string double _similarity(String a, String b) { if (a.isEmpty || b.isEmpty) return 0.0; final aWords = a.split(' '); final bWords = b.split(' '); int matches = 0; for (var aw in aWords) { for (var bw in bWords) { if (aw.isNotEmpty && bw.isNotEmpty && aw == bw) matches++; } } return matches / ((aWords.length + bWords.length) / 2); } /// 🔹 Ekstrak data dari hasil OCR Map parseResiData(String text, String storeId) { final lines = text.split('\n').map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); final RegExp penerimaExp = RegExp(r'Penerima\s*:\s*(.*)', caseSensitive: false); final RegExp alamatExp = RegExp(r'Alamat\s*:\s*(.*)', caseSensitive: false); final RegExp waExp = RegExp(r'(No\.?\s*Wa|Nomor\s*Wa|WA)\s*:\s*(.*)', caseSensitive: false); final RegExp barangExp = RegExp(r'Barang\s*:\s*(.*)', caseSensitive: false); final RegExp totalExp = RegExp(r'Total\s*:\s*([0-9.,]+)', caseSensitive: false); final RegExp pembayaranExp = RegExp(r'(COD|Non\s*COD)', caseSensitive: false); String penerima = ''; String alamat = ''; String noWa = ''; String barang = ''; String total = ''; String pembayaran = ''; bool inAlamatSection = false; for (int i = 0; i < lines.length; i++) { final line = lines[i]; if (penerimaExp.hasMatch(line)) { penerima = penerimaExp.firstMatch(line)?.group(1)?.trim() ?? ''; } else if (alamatExp.hasMatch(line)) { // mulai tangkap alamat alamat = alamatExp.firstMatch(line)?.group(1)?.trim() ?? ''; inAlamatSection = true; continue; } else if (waExp.hasMatch(line)) { inAlamatSection = false; noWa = waExp.firstMatch(line)?.group(2)?.trim() ?? ''; } else if (barangExp.hasMatch(line)) { inAlamatSection = false; barang = barangExp.firstMatch(line)?.group(1)?.trim() ?? ''; } else if (totalExp.hasMatch(line)) { inAlamatSection = false; total = totalExp.firstMatch(line)?.group(1)?.trim() ?? ''; total = total.replaceAll(RegExp(r'[^\d]'), ''); } else if (pembayaranExp.hasMatch(line)) { inAlamatSection = false; pembayaran = pembayaranExp.firstMatch(line)?.group(1)?.trim().toUpperCase() ?? ''; if (pembayaran.contains('NON')) { pembayaran = 'NON COD'; } else { pembayaran = 'COD'; } } else if (inAlamatSection) { // gabungkan baris tambahan alamat if (line.isNotEmpty && !line.contains(':')) { alamat += ' $line'; } } } // 🔍 Ekstrak kota & provinsi dari alamat lengkap final wilayah = _extractWilayah(alamat); return { 'store_id': storeId, 'penerima': penerima, 'alamat': alamat, 'no_wa': noWa, 'barang': barang, 'total': total, 'pembayaran': pembayaran, 'kota': wilayah['kota'], 'provinsi': wilayah['provinsi'], 'created_at': FieldValue.serverTimestamp(), }; } /// 🔹 Simpan hasil OCR ke Firestore Future saveResi(String ocrText, String userId) async { try { final storeRef = await _firestore.collection('stores').doc(userId).get(); // 🔹 Cek apakah user punya toko if (!storeRef.exists) { Get.snackbar( 'Gagal Menyimpan', 'Kamu belum memiliki data toko.\nSilakan isi data toko terlebih dahulu.', backgroundColor: const Color(0xFFFF9800), colorText: Colors.white, snackPosition: SnackPosition.BOTTOM, ); return; } final storeId = storeRef.id; // 🔹 Parsing hasil OCR final data = parseResiData(ocrText, storeId); // 🔹 Validasi field utama List missing = []; if (data['penerima'].toString().isEmpty) missing.add('Penerima'); if (data['alamat'].toString().isEmpty) missing.add('Alamat'); if (data['no_wa'].toString().isEmpty) missing.add('No. WA'); if (data['barang'].toString().isEmpty) missing.add('Barang'); if (data['total'].toString().isEmpty) missing.add('Total'); if (data['pembayaran'].toString().isEmpty) missing.add('Pembayaran'); if (missing.isNotEmpty) { Get.snackbar( 'Gagal Menyimpan', 'Field berikut kosong:\n${missing.join(', ')}', backgroundColor: const Color(0xFFFF9800), colorText: Colors.white, snackPosition: SnackPosition.BOTTOM, ); return; } // 🔥 VALIDASI WILAYAH (INI YANG KAMU BUTUHKAN) if (data['provinsi'] == null || data['provinsi'].toString().isEmpty) { Get.snackbar( 'Gagal Menyimpan', 'Provinsi tidak terdeteksi dari alamat!', backgroundColor: const Color(0xFFFF9800), colorText: Colors.white, snackPosition: SnackPosition.BOTTOM, ); return; } if (data['kota'] == null || data['kota'].toString().isEmpty) { Get.snackbar( 'Gagal Menyimpan', 'Kota/Kabupaten tidak terdeteksi!', backgroundColor: const Color(0xFFFF9800), colorText: Colors.white, snackPosition: SnackPosition.BOTTOM, ); return; } // 🔹 Simpan ke Firestore await _firestore.collection('resis').add(data); Get.snackbar( 'Berhasil', 'Data resi berhasil disimpan!', backgroundColor: const Color(0xFF4CAF50), colorText: Colors.white, snackPosition: SnackPosition.BOTTOM, ); } catch (e) { Get.snackbar( 'Error', 'Gagal menyimpan data: $e', backgroundColor: const Color(0xFFF44336), colorText: Colors.white, snackPosition: SnackPosition.BOTTOM, ); } } // Future saveResi(String ocrText, String userId) async { // /// ✅ cegah double klik // if (isSaving.value) return; // /// ✅ CEK INTERNET GLOBAL // final internet = Get.find(); // if (!internet.isConnected.value) { // Get.snackbar( // 'Offline', // 'Tidak dapat menyimpan tanpa koneksi internet.', // backgroundColor: const Color(0xFFFF9800), // colorText: Colors.white, // snackPosition: SnackPosition.BOTTOM, // ); // return; // } // try { // isSaving.value = true; // final storeRef = // await _firestore.collection('stores').doc(userId).get(); // if (!storeRef.exists) { // Get.snackbar( // 'Gagal Menyimpan', // 'Kamu belum memiliki data toko.\nSilakan isi data toko terlebih dahulu.', // backgroundColor: const Color(0xFFFF9800), // colorText: Colors.white, // snackPosition: SnackPosition.BOTTOM, // ); // return; // } // final storeId = storeRef.id; // final data = parseResiData(ocrText, storeId); // /// =============================== // /// VALIDASI FIELD OCR // /// =============================== // List missing = []; // if (data['penerima'].toString().trim().isEmpty) // missing.add('Penerima'); // if (data['alamat'].toString().trim().isEmpty) // missing.add('Alamat'); // if (data['no_wa'].toString().trim().isEmpty) // missing.add('No. WA'); // if (data['barang'].toString().trim().isEmpty) // missing.add('Barang'); // if (data['total'].toString().trim().isEmpty) // missing.add('Total'); // if (data['pembayaran'].toString().trim().isEmpty) // missing.add('Pembayaran'); // if (missing.isNotEmpty) { // Get.snackbar( // 'Gagal Menyimpan', // 'Field berikut kosong:\n${missing.join(', ')}', // backgroundColor: const Color(0xFFFF9800), // colorText: Colors.white, // snackPosition: SnackPosition.BOTTOM, // ); // return; // } // /// =============================== // /// ✅ ANTI DUPLICATE FIRESTORE // /// =============================== // /// buat document id unik dari data resi // final docId = // "${data['no_wa']}_${data['total']}_${data['penerima']}"; // await _firestore // .collection('resis') // .doc(docId) // .set(data); // ✅ BUKAN add() // Get.snackbar( // 'Berhasil', // 'Data resi berhasil disimpan!', // backgroundColor: const Color(0xFF4CAF50), // colorText: Colors.white, // snackPosition: SnackPosition.BOTTOM, // ); // } on FirebaseException catch (e) { // /// ✅ HANDLE INTERNET PUTUS TENGAH JALAN // if (e.code == 'network-request-failed') { // Get.snackbar( // 'Koneksi Terputus', // 'Internet tidak stabil.', // backgroundColor: const Color(0xFFFF9800), // colorText: Colors.white, // snackPosition: SnackPosition.BOTTOM, // ); // } else { // Get.snackbar( // 'Error Firebase', // e.message ?? 'Terjadi kesalahan.', // backgroundColor: const Color(0xFFF44336), // colorText: Colors.white, // snackPosition: SnackPosition.BOTTOM, // ); // } // } catch (e) { // Get.snackbar( // 'Error', // 'Gagal menyimpan data.', // backgroundColor: const Color(0xFFF44336), // colorText: Colors.white, // snackPosition: SnackPosition.BOTTOM, // ); // } finally { // /// ✅ aktifkan button kembali // isSaving.value = false; // } // } }