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

569 lines
17 KiB
Dart

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<Map<String, dynamic>> _wilayahData = [];
var monthlySummary = <Map<String, dynamic>>[].obs;
var isLoading = false.obs;
var dailySummary = <Map<String, dynamic>>[].obs;
var dailyDetail = <Map<String, dynamic>>[].obs;
/// 🔹 Ambil data ringkasan resi per hari
Future<void> 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<String, List<Map<String, dynamic>>> grouped = {};
for (var item in data) {
grouped.putIfAbsent(item['tanggalKey'], () => []);
grouped[item['tanggalKey']]!.add(item);
}
final List<Map<String, dynamic>> 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<void> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<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;
}
}
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<void> updateResi(String id, Map<String, dynamic> 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<void> updateResi(String id, Map<String, dynamic> 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<String> 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<void> 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<void> 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);
}
}
}