505 lines
15 KiB
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;
|
|
// }
|
|
// }
|
|
|
|
} |