E41220983_MuhamadSugengCahy.../praresi/lib/presentation/controllers/resi_controller.dart

505 lines
15 KiB
Dart

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<Map<String, dynamic>> _wilayahData = [];
var isSaving = false.obs; // ✅ loading save
@override
void onInit() {
super.onInit();
_loadWilayahData();
}
/// 🔹 Muat data provinsi & kota dari JSON lokal
Future<void> _loadWilayahData() async {
try {
final String jsonString = await rootBundle.loadString('assets/data/regions.json');
final List<dynamic> jsonData = jsonDecode(jsonString);
_wilayahData = jsonData.cast<Map<String, dynamic>>();
} catch (e) {
debugPrint("Gagal memuat data wilayah: $e");
}
}
/// 🔹 Deteksi provinsi & kota dari alamat (prioritaskan akhir alamat + fuzzy + aman)
// Map<String, String?> _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<String> 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<String, String?> _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<String> 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<String, dynamic> 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<void> 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<String> 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<void> saveResi(String ocrText, String userId) async {
// /// ✅ cegah double klik
// if (isSaving.value) return;
// /// ✅ CEK INTERNET GLOBAL
// final internet = Get.find<InternetController>();
// 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<String> 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;
// }
// }
}