import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:intl/intl.dart'; class RiwayatController extends GetxController { final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseAuth _auth = FirebaseAuth.instance; List> _wilayahData = []; var monthlySummary = >[].obs; var isLoading = false.obs; var dailySummary = >[].obs; var dailyDetail = >[].obs; /// 🔹 Ambil data ringkasan resi per hari Future fetchResiData({DateTime? start, DateTime? end}) async { try { isLoading.value = true; final user = _auth.currentUser; if (user == null) return; final userId = user.uid; final DateTime now = DateTime.now(); final DateTime startOfMonth = start ?? DateTime(now.year, now.month, 1); final DateTime endOfMonth = end ?? DateTime(now.year, now.month + 1, 0, 23, 59, 59); final snapshot = await _firestore .collection('resis') .where('store_id', isEqualTo: userId) .where('created_at', isGreaterThanOrEqualTo: startOfMonth) .where('created_at', isLessThanOrEqualTo: endOfMonth) .get(); final data = snapshot.docs.map((doc) { final resi = doc.data(); final createdAt = (resi['created_at'] as Timestamp).toDate(); final tanggalKey = DateFormat('yyyy-MM-dd').format(createdAt); return { 'id': doc.id, 'tanggalKey': tanggalKey, 'tanggalAsli': createdAt, 'pembayaran': resi['pembayaran'] ?? '', 'total': int.tryParse(resi['total'].toString()) ?? 0, }; }).toList(); final Map>> grouped = {}; for (var item in data) { grouped.putIfAbsent(item['tanggalKey'], () => []); grouped[item['tanggalKey']]!.add(item); } final List> summary = []; grouped.forEach((key, list) { int total = 0, codCount = 0, nonCodCount = 0, income = 0; for (var resi in list) { total += 1; final totalValue = num.tryParse(resi['total'].toString()) ?? 0; income += totalValue.toInt(); if (resi['pembayaran'].toString().toUpperCase() == 'COD') { codCount++; } else { nonCodCount++; } } final date = DateTime.parse(key); summary.add({ 'day': DateFormat('EEEE', 'id_ID').format(date), 'date': DateFormat('d MMMM yyyy', 'id_ID').format(date), 'total': total, 'cod': codCount, 'nonCod': nonCodCount, 'income': income, 'rawDate': date, }); }); summary.sort((a, b) => b['rawDate'].compareTo(a['rawDate'])); dailySummary.assignAll(summary); } catch (e) { print('Error fetching resi data: $e'); dailySummary.clear(); } finally { isLoading.value = false; } } /// 🔹 Ambil data resi + detail toko berdasarkan tanggal Future fetchResiByDate(DateTime selectedDate) async { try { isLoading.value = true; final user = _auth.currentUser; if (user == null) return; final userId = user.uid; final startOfDay = DateTime(selectedDate.year, selectedDate.month, selectedDate.day); final endOfDay = DateTime(selectedDate.year, selectedDate.month, selectedDate.day, 23, 59, 59); final snapshot = await _firestore .collection('resis') .where('store_id', isEqualTo: userId) .where('created_at', isGreaterThanOrEqualTo: startOfDay) .where('created_at', isLessThanOrEqualTo: endOfDay) .get(); // 🔹 Ambil data resi & data toko List> results = []; for (var doc in snapshot.docs) { final data = doc.data(); final storeId = data['store_id']; // ambil data toko final storeSnapshot = await _firestore.collection('stores').doc(storeId).get(); final storeData = storeSnapshot.data() ?? {}; results.add({ 'id': doc.id, 'no_resi': data['no_resi'] ?? '-', 'penerima': data['penerima'] ?? '-', 'alamat': data['alamat'] ?? '-', 'kota': data['kota'] ?? '-', 'provinsi': data['provinsi'] ?? '-', 'barang': data['barang'] ?? '-', 'pembayaran': data['pembayaran'] ?? '-', 'total': data['total'] ?? 0, 'no_wa': data['no_wa'] ?? '-', 'created_at': (data['created_at'] as Timestamp).toDate(), 'store': { 'namaToko': storeData['namaToko'] ?? '-', 'alamat': storeData['alamat'] ?? '-', 'noHp': storeData['noHp'] ?? '-', 'noRegistrasi': storeData['noRegistrasi'] ?? '-', 'keterangan': storeData['keterangan'] ?? '-', } }); } dailyDetail.assignAll(results); } catch (e) { print('Error fetching resi detail: $e'); dailyDetail.clear(); } finally { isLoading.value = false; } } /// 🔹 Ambil total pendapatan COD & Non COD per tanggal Future> getPendapatanHarian(DateTime date) async { try { final user = _auth.currentUser; if (user == null) return {}; final startOfDay = DateTime(date.year, date.month, date.day); final endOfDay = DateTime(date.year, date.month, date.day, 23, 59, 59); final snapshot = await _firestore .collection('resis') .where('store_id', isEqualTo: user.uid) .where('created_at', isGreaterThanOrEqualTo: startOfDay) .where('created_at', isLessThanOrEqualTo: endOfDay) .get(); double codIncome = 0; double nonCodIncome = 0; for (var doc in snapshot.docs) { final data = doc.data(); final pembayaran = (data['pembayaran'] ?? '').toString().toUpperCase(); final total = double.tryParse(data['total'].toString()) ?? 0; if (pembayaran == 'COD') { codIncome += total; } else { nonCodIncome += total; } } return { 'cod': codIncome, 'nonCod': nonCodIncome, }; } catch (e) { print('Error getPendapatanHarian: $e'); return {}; } } 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; } } return { 'kota': kota, 'provinsi': provinsi, }; } 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); } /// 🔹 Update data resi berdasarkan ID Future updateResi(String id, Map updatedData) async { try { isLoading.value = true; // Pastikan dokumen dengan ID tersebut ada final docRef = _firestore.collection('resis').doc(id); final snapshot = await docRef.get(); if (!snapshot.exists) { Get.snackbar( "Gagal", "Data resi tidak ditemukan.", snackPosition: SnackPosition.BOTTOM, backgroundColor: const Color(0xFFFFCDD2), colorText: const Color(0xFFB71C1C), ); return; } // 🔹 Update data di Firestore await docRef.update(updatedData); // 🔹 Refresh data setelah update if (snapshot.data()?['created_at'] != null) { final tanggal = (snapshot.data()!['created_at'] as Timestamp).toDate(); await fetchResiByDate(tanggal); } Get.snackbar( "Berhasil", "Data resi berhasil diperbarui.", snackPosition: SnackPosition.BOTTOM, backgroundColor: const Color(0xFFC8E6C9), colorText: const Color(0xFF1B5E20), ); } catch (e) { print("Error update resi: $e"); Get.snackbar( "Error", "Terjadi kesalahan saat memperbarui data.", snackPosition: SnackPosition.BOTTOM, backgroundColor: const Color(0xFFFFCDD2), colorText: const Color(0xFFB71C1C), ); } finally { isLoading.value = false; } } // Future updateResi(String id, Map updatedData) async { // try { // isLoading.value = true; // final docRef = _firestore.collection('resis').doc(id); // final snapshot = await docRef.get(); // if (!snapshot.exists) { // Get.snackbar( // "Gagal", // "Data resi tidak ditemukan.", // snackPosition: SnackPosition.BOTTOM, // backgroundColor: const Color(0xFFFFCDD2), // colorText: const Color(0xFFB71C1C), // ); // return; // } // final oldData = snapshot.data()!; // // ============================== // // 🔥 VALIDASI FIELD KOSONG // // ============================== // bool isEmpty(String? val) => val == null || val.trim().isEmpty; // List missing = []; // if (isEmpty(updatedData['penerima'])) missing.add('Penerima'); // if (isEmpty(updatedData['alamat'])) missing.add('Alamat'); // if (isEmpty(updatedData['no_wa'])) missing.add('No. WA'); // if (isEmpty(updatedData['barang'])) missing.add('Barang'); // if (isEmpty(updatedData['total'])) missing.add('Total'); // if (isEmpty(updatedData['pembayaran'])) missing.add('Pembayaran'); // if (missing.isNotEmpty) { // Get.snackbar( // 'Gagal Update', // 'Field berikut kosong:\n${missing.join(', ')}', // backgroundColor: const Color(0xFFFF9800), // colorText: Colors.white, // snackPosition: SnackPosition.BOTTOM, // ); // return; // } // // ============================== // // 🔥 VALIDASI FORMAT // // ============================== // // No WA minimal 10 digit angka // String noWa = // updatedData['no_wa'].toString().replaceAll(RegExp(r'\D'), ''); // if (noWa.length < 10) { // Get.snackbar( // 'Gagal Update', // 'No. WA tidak valid!', // backgroundColor: const Color(0xFFFF9800), // colorText: Colors.white, // snackPosition: SnackPosition.BOTTOM, // ); // return; // } // // Total harus angka // String total = // updatedData['total'].toString().replaceAll(RegExp(r'\D'), ''); // if (total.isEmpty || int.tryParse(total) == null) { // Get.snackbar( // 'Gagal Update', // 'Total harus berupa angka!', // backgroundColor: const Color(0xFFFF9800), // colorText: Colors.white, // snackPosition: SnackPosition.BOTTOM, // ); // return; // } // // ============================== // // 🔥 CEK APAKAH ALAMAT DIUBAH // // ============================== // final oldAlamat = oldData['alamat'] ?? ''; // final newAlamat = updatedData['alamat'] ?? ''; // // ============================== // // 🔥 SELALU DETEKSI ULANG WILAYAH // // ============================== // final alamat = updatedData['alamat'].toString().trim(); // final wilayah = _extractWilayah(alamat); // // ❗ Kalau gagal detect → TOLAK // if (wilayah['provinsi'] == null || wilayah['kota'] == null) { // Get.snackbar( // 'Gagal Update', // 'Alamat tidak valid, provinsi/kota tidak terdeteksi!', // backgroundColor: const Color(0xFFFF9800), // colorText: Colors.white, // snackPosition: SnackPosition.BOTTOM, // ); // return; // } // // 🔥 SET ULANG // updatedData['alamat'] = alamat; // updatedData['kota'] = wilayah['kota']; // updatedData['provinsi'] = wilayah['provinsi']; // // ============================== // // 🔥 CLEAN DATA // // ============================== // updatedData.updateAll((key, value) => value.toString().trim()); // debugPrint("FINAL UPDATE DATA: $updatedData"); // // ============================== // // 🔹 UPDATE FIRESTORE // // ============================== // await docRef.update(updatedData); // // 🔹 Refresh data // if (snapshot.data()?['created_at'] != null) { // final tanggal = // (snapshot.data()!['created_at'] as Timestamp).toDate(); // await fetchResiByDate(tanggal); // } // Get.snackbar( // "Berhasil", // "Data resi berhasil diperbarui.", // snackPosition: SnackPosition.BOTTOM, // backgroundColor: const Color(0xFFC8E6C9), // colorText: const Color(0xFF1B5E20), // ); // } catch (e) { // debugPrint("Error update resi: $e"); // Get.snackbar( // "Error", // "Terjadi kesalahan saat memperbarui data.", // snackPosition: SnackPosition.BOTTOM, // backgroundColor: const Color(0xFFFFCDD2), // colorText: const Color(0xFFB71C1C), // ); // } finally { // isLoading.value = false; // } // } Future deleteRiwayat(String id) async { try { isLoading(true); // Hapus dari Firestore await _firestore.collection('riwayat_bulanan').doc(id).delete(); // Hapus juga dari list lokal agar tampilan otomatis ter-update monthlySummary.removeWhere((item) => item['id'] == id); Get.snackbar( "Berhasil", "Data berhasil dihapus", backgroundColor: const Color(0xFFFFCDD2), snackPosition: SnackPosition.BOTTOM, ); } catch (e) { Get.snackbar( "Error", "Gagal menghapus data: $e", backgroundColor: const Color(0xFFFFE0B2), snackPosition: SnackPosition.BOTTOM, ); } finally { isLoading(false); } } /// 🔹 Hapus data resi berdasarkan ID dokumen Future deleteResi(String id) async { try { isLoading(true); // Cek apakah dokumen ada final docRef = _firestore.collection('resis').doc(id); final snapshot = await docRef.get(); if (!snapshot.exists) { Get.snackbar( "Gagal", "Data tidak ditemukan atau sudah dihapus.", snackPosition: SnackPosition.BOTTOM, backgroundColor: const Color(0xFFFFCDD2), colorText: const Color(0xFFB71C1C), ); return; } // 🔹 Hapus dari Firestore await docRef.delete(); // 🔹 Hapus dari list lokal agar tampilan otomatis update dailyDetail.removeWhere((item) => item['id'] == id); dailySummary.removeWhere((item) => item['id'] == id); Get.snackbar( "Berhasil", "Data berhasil dihapus.", snackPosition: SnackPosition.BOTTOM, backgroundColor: const Color(0xFFFFEBEE), colorText: const Color(0xFFB71C1C), ); } catch (e) { Get.snackbar( "Error", "Gagal menghapus data: $e", snackPosition: SnackPosition.BOTTOM, backgroundColor: const Color(0xFFFFE0B2), colorText: const Color(0xFFBF360C), ); } finally { isLoading(false); } } }