TKK_E32231105/lib/providers/panen_provider.dart

672 lines
21 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart';
import 'package:firebase_database/firebase_database.dart';
import '../models/panen_model.dart';
import '../services/notification_service.dart';
class PanenProvider extends ChangeNotifier {
final FirebaseDatabase _database = FirebaseDatabase.instance;
static const bool _allowFlutterAutoCaptureWrite = false;
// Tracking snapshot untuk time-windowing logic
final Map<String, int> _snapshotPagiHariIni = {}; // kandangId -> nilaiPagi
final Map<String, int> _snapshotSoreHariIni = {}; // kandangId -> nilaiSore
final List<Panen> _panens = [];
final Set<String> _knownRealtimeRecordKeys = <String>{};
StreamSubscription<DatabaseEvent>? _recordsRealtimeSubscription;
bool _realtimeListenerStarted = false;
List<Panen> get panens => _panens;
bool _isKandang1(String kandangId) {
final normalized = kandangId.toLowerCase().replaceAll('_', '');
return normalized == 'kandang1';
}
bool _isKandang2(String kandangId) {
final normalized = kandangId.toLowerCase().replaceAll('_', '');
return normalized == 'kandang2';
}
String _jenisFromPanen(Panen panen) {
final jenis = (panen.jenisPanen ?? '').trim().toLowerCase();
if (jenis == 'pagi' || jenis == 'sore') return jenis;
// Parse jam untuk klasifikasi: < 12 adalah pagi, >= 12 adalah sore
final jam = panen.jam.trim();
final jamParts = jam.split(':');
if (jamParts.isNotEmpty) {
final hour = int.tryParse(jamParts[0]);
if (hour != null) {
return hour < 12 ? 'pagi' : 'sore';
}
}
return 'panen';
}
bool _isSameDate(DateTime a, DateTime b) {
return a.year == b.year && a.month == b.month && a.day == b.day;
}
Future<void> _notifySummaryForJenis(Panen latestPanen) async {
final jenis = _jenisFromPanen(latestPanen);
if (jenis != 'pagi' && jenis != 'sore') return;
int k1 = 0;
int k2 = 0;
for (final panen in _panens) {
if (!_isSameDate(panen.tanggalPanen, latestPanen.tanggalPanen)) continue;
if (_jenisFromPanen(panen) != jenis) continue;
if (_isKandang1(panen.kandangId)) {
k1 += panen.jumlahTelur;
} else if (_isKandang2(panen.kandangId)) {
k2 += panen.jumlahTelur;
}
}
final totals = <String, int>{
'kandang 1': k1,
'kandang 2': k2,
};
await NotificationService.instance.showPanenSummaryNotification(
jenisPanen: jenis,
kandangTotals: totals,
);
}
Future<void> startRealtimePanenNotifications() async {
if (_realtimeListenerStarted) return;
final ref = _database.ref('riwayat/records');
try {
final snapshot = await ref.get();
if (snapshot.exists && snapshot.value is Map) {
final data = Map<dynamic, dynamic>.from(snapshot.value as Map);
for (final entry in data.entries) {
_knownRealtimeRecordKeys.add(entry.key.toString());
}
}
} catch (e) {
if (kDebugMode) {
print('⚠️ Seed realtime panen keys gagal: $e');
}
}
_recordsRealtimeSubscription = ref.onChildAdded.listen((event) async {
try {
final key = event.snapshot.key;
if (key == null || _knownRealtimeRecordKeys.contains(key)) return;
_knownRealtimeRecordKeys.add(key);
if (!event.snapshot.exists || event.snapshot.value is! Map) return;
final panenData =
Map<dynamic, dynamic>.from(event.snapshot.value as Map);
final tanggalRaw = panenData['tanggal_panen'];
final tanggal = DateTime.tryParse('${tanggalRaw ?? ''}');
if (tanggal == null) return;
final newPanen = Panen(
id: panenData['id'] ?? '',
kandangId: panenData['kandang_id'] ?? '',
kandangNama: panenData['kandang_nama'] ?? '',
jumlahTelur: panenData['jumlah_telur'] ?? 0,
tanggalPanen: tanggal,
jam: panenData['jam'] ?? '',
catatan: panenData['catatan'] ?? '',
jenisPanen: panenData['jenis_panen'],
sensorSnapshot: panenData['sensor_snapshot'],
panenSebelumnya: panenData['panen_sebelumnya'],
);
final exists =
_panens.any((p) => p.id == newPanen.id && p.id.isNotEmpty);
if (!exists) {
_panens.add(newPanen);
_panens.sort((a, b) => b.tanggalPanen.compareTo(a.tanggalPanen));
notifyListeners();
}
await _notifySummaryForJenis(newPanen);
} catch (e) {
if (kDebugMode) {
print('⚠️ Realtime panen notification error: $e');
}
}
});
_realtimeListenerStarted = true;
if (kDebugMode) {
print('✅ Realtime panen notification listener started');
}
}
Future<void> stopRealtimePanenNotifications() async {
await _recordsRealtimeSubscription?.cancel();
_recordsRealtimeSubscription = null;
_realtimeListenerStarted = false;
_knownRealtimeRecordKeys.clear();
}
List<Panen> getPanenByKandang(String kandangId) {
return _panens.where((p) => p.kandangId == kandangId).toList();
}
List<Panen> getPanenByDate(DateTime date) {
return _panens.where((p) {
return p.tanggalPanen.year == date.year &&
p.tanggalPanen.month == date.month &&
p.tanggalPanen.day == date.day;
}).toList();
}
List<Panen> getPanenByDateRange(DateTime startDate, DateTime endDate) {
return _panens.where((p) {
return p.tanggalPanen.isAfter(startDate) &&
p.tanggalPanen.isBefore(endDate.add(const Duration(days: 1)));
}).toList();
}
Panen addPanen(
String kandangId,
String kandangNama,
int jumlahTelur,
DateTime tanggalPanen,
String jam,
String catatan,
) {
const uuid = Uuid();
final newPanen = Panen(
id: uuid.v4(),
kandangId: kandangId,
kandangNama: kandangNama,
jumlahTelur: jumlahTelur,
tanggalPanen: tanggalPanen,
jam: jam,
catatan: catatan,
);
_panens.add(newPanen);
_panens.sort((a, b) => b.tanggalPanen.compareTo(a.tanggalPanen));
notifyListeners();
return newPanen;
}
void deletePanen(String id) {
_panens.removeWhere((p) => p.id == id);
notifyListeners();
}
int getTotalPanenHari(DateTime date, String kandangId) {
return _panens.where((p) {
return p.kandangId == kandangId &&
p.tanggalPanen.year == date.year &&
p.tanggalPanen.month == date.month &&
p.tanggalPanen.day == date.day;
}).fold(0, (sum, p) => sum + p.jumlahTelur);
}
/// Get total panen per hari untuk semua kandang
int getTotalHarianSemuaKandang(DateTime date) {
return _panens.where((p) {
return p.tanggalPanen.year == date.year &&
p.tanggalPanen.month == date.month &&
p.tanggalPanen.day == date.day;
}).fold(0, (sum, p) => sum + p.jumlahTelur);
}
/// Get last harvest info untuk kandang
/// Berguna untuk dashboard "Status Panen Terakhir"
Panen? getLastHarvestStatus(String kandangId) {
final panenKandang =
_panens.where((p) => p.kandangId == kandangId).toList();
if (panenKandang.isEmpty) return null;
panenKandang.sort((a, b) => b.tanggalPanen.compareTo(a.tanggalPanen));
return panenKandang.first;
}
/// Get panen history per kandang grouped by date untuk riwayat
Map<DateTime, List<Panen>> getPanenHistoryGroupedByDate(String kandangId) {
final map = <DateTime, List<Panen>>{};
final panenKandang =
_panens.where((p) => p.kandangId == kandangId).toList();
for (final panen in panenKandang) {
final dateKey = DateTime(
panen.tanggalPanen.year,
panen.tanggalPanen.month,
panen.tanggalPanen.day,
);
map.putIfAbsent(dateKey, () => []).add(panen);
}
return map;
}
/// Add panen dari sensor snapshot dengan tracking jenis panen (pagi/sore)
Panen addPanenFromSensor(
String kandangId,
String kandangNama,
int sensorValue,
String jenisPanen,
String jam, {
int? panenSebelumnya,
}) {
const uuid = Uuid();
// Jika panen sore dan ada panen pagi, hitung delta
int jumlahTelur = sensorValue;
if (jenisPanen == 'sore' && panenSebelumnya != null) {
jumlahTelur = sensorValue - panenSebelumnya;
}
final newPanen = Panen(
id: uuid.v4(),
kandangId: kandangId,
kandangNama: kandangNama,
jumlahTelur: jumlahTelur,
tanggalPanen: DateTime.now(),
jam: jam,
catatan: 'Panen $jenisPanen - Auto capture dari sensor',
jenisPanen: jenisPanen,
panenSebelumnya: panenSebelumnya,
sensorSnapshot: sensorValue,
);
_panens.add(newPanen);
_panens.sort((a, b) => b.tanggalPanen.compareTo(a.tanggalPanen));
notifyListeners();
return newPanen;
}
// ============================================
// TIME-WINDOWING & SNAPSHOT TRACKING
// ============================================
/// Get nilai panen pagi hari ini untuk delta calculation sore
int? getPanenPagiTodayValue(String kandangId) {
return _snapshotPagiHariIni[kandangId];
}
/// Get nilai panen sore hari ini
int? getPanenSoreTodayValue(String kandangId) {
return _snapshotSoreHariIni[kandangId];
}
/// Cek apakah panen pagi sudah di-record hari ini
bool isPanenPagiRecordedToday(String kandangId) {
return _snapshotPagiHariIni.containsKey(kandangId);
}
/// Cek apakah panen sore sudah di-record hari ini
bool isPanenSoreRecordedToday(String kandangId) {
return _snapshotSoreHariIni.containsKey(kandangId);
}
/// Reset snapshot harian (call setelah panen sore selesai atau tengah malam)
void resetDailySnapshots() {
_snapshotPagiHariIni.clear();
_snapshotSoreHariIni.clear();
if (kDebugMode) {
print('✅ Daily snapshots reset');
}
notifyListeners();
}
/// AUTO-CAPTURE DARI SCHEDULER
/// Panggil ini saat scheduler trigger jam 09:00
Future<Panen?> captureScheduledPanenPagi(
String kandangId,
String kandangNama,
int sensorValue,
) async {
try {
// Jika sudah di-record pagi ini, skip
if (isPanenPagiRecordedToday(kandangId)) {
if (kDebugMode) {
print('⚠️ Panen pagi untuk $kandangId sudah di-record hari ini');
}
return null;
}
// Time-Window Pagi: Ambil nilai sensor sebagai total telur pagi
// (Karena ini akumulasi dari jam 15:01 kemarin hingga 09:00 hari ini)
int jumlahTelur = sensorValue;
// Simpan ke snapshot untuk reference delta sore nanti
_snapshotPagiHariIni[kandangId] = sensorValue;
const uuid = Uuid();
final now = DateTime.now();
final jamStr =
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
final newPanen = Panen(
id: uuid.v4(),
kandangId: kandangId,
kandangNama: kandangNama,
jumlahTelur: jumlahTelur,
tanggalPanen: now,
jam: jamStr,
catatan: 'Auto-capture PAGI dari scheduler',
jenisPanen: 'pagi',
sensorSnapshot: sensorValue,
panenSebelumnya: null, // Pagi tidak butuh panenSebelumnya
);
_panens.add(newPanen);
_panens.sort((a, b) => b.tanggalPanen.compareTo(a.tanggalPanen));
// Guard: auto-capture write hanya dari Railway worker.
if (_allowFlutterAutoCaptureWrite) {
await _savePanenToFirebase(newPanen);
}
if (kDebugMode) {
print('✅ Panen Pagi recorded untuk $kandangNama: $jumlahTelur telur');
}
notifyListeners();
return newPanen;
} catch (e) {
if (kDebugMode) {
print('❌ Error capturing panen pagi: $e');
}
return null;
}
}
/// AUTO-CAPTURE DARI SCHEDULER
/// Panggil ini saat scheduler trigger jam 15:00
/// Dengan delta calculation: nilai_sore - nilai_pagi
Future<Panen?> captureScheduledPanenSore(
String kandangId,
String kandangNama,
int sensorValue,
) async {
try {
// Jika sudah di-record sore ini, skip
if (isPanenSoreRecordedToday(kandangId)) {
if (kDebugMode) {
print('⚠️ Panen sore untuk $kandangId sudah di-record hari ini');
}
return null;
}
// Ambil nilai pagi hari ini
int? nilaiPagi = getPanenPagiTodayValue(kandangId);
// Time-Window Sore: Hitung delta dari nilai pagi
int jumlahTelur = sensorValue;
if (nilaiPagi != null) {
jumlahTelur = sensorValue - nilaiPagi;
} else {
if (kDebugMode) {
print(
'⚠️ Nilai pagi tidak ditemukan untuk $kandangId, gunakan nilai sensor langsung',
);
}
}
// Simpan ke snapshot
_snapshotSoreHariIni[kandangId] = sensorValue;
const uuid = Uuid();
final now = DateTime.now();
final jamStr =
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
final newPanen = Panen(
id: uuid.v4(),
kandangId: kandangId,
kandangNama: kandangNama,
jumlahTelur: jumlahTelur,
tanggalPanen: now,
jam: jamStr,
catatan:
'Auto-capture SORE dari scheduler${nilaiPagi != null ? ' (delta: $sensorValue - $nilaiPagi)' : ''}',
jenisPanen: 'sore',
sensorSnapshot: sensorValue,
panenSebelumnya: nilaiPagi,
);
_panens.add(newPanen);
_panens.sort((a, b) => b.tanggalPanen.compareTo(a.tanggalPanen));
// Guard: auto-capture write hanya dari Railway worker.
if (_allowFlutterAutoCaptureWrite) {
await _savePanenToFirebase(newPanen);
}
if (kDebugMode) {
print(
'✅ Panen Sore recorded untuk $kandangNama: $jumlahTelur telur${nilaiPagi != null ? ' (delta: $sensorValue - $nilaiPagi)' : ''}',
);
}
notifyListeners();
return newPanen;
} catch (e) {
if (kDebugMode) {
print('❌ Error capturing panen sore: $e');
}
return null;
}
}
/// Simpan panen ke Firebase
Future<void> _savePanenToFirebase(Panen panen) async {
try {
final ref = _database.ref('riwayat/records').push();
await ref.set({
'id': panen.id,
'kandang_id': panen.kandangId,
'kandang_nama': panen.kandangNama,
'jumlah_telur': panen.jumlahTelur,
'tanggal_panen': panen.tanggalPanen.toIso8601String(),
'jam': panen.jam,
'jenis_panen': panen.jenisPanen,
'sensor_snapshot': panen.sensorSnapshot,
'panen_sebelumnya': panen.panenSebelumnya,
'catatan': panen.catatan,
});
await _updateRiwayatSummary(panen);
if (kDebugMode) {
print('✅ Panen saved to Firebase: ${panen.id}');
}
} catch (e) {
if (kDebugMode) {
print('❌ Error saving panen to Firebase: $e');
}
}
}
Future<void> _updateRiwayatSummary(Panen panen) async {
final now = panen.tanggalPanen;
final dateKey =
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
final summaryRef = _database.ref('riwayat/summary');
await summaryRef.runTransaction((current) {
final data = Map<dynamic, dynamic>.from(
(current as Map?) ?? <dynamic, dynamic>{},
);
int telurHariIni = (data['telur_hari_ini'] ?? 0) as int;
int totalTelur = (data['total_telur'] ?? 0) as int;
int kandang1 = (data['kandang1_hari_ini'] ?? 0) as int;
int kandang2 = (data['kandang2_hari_ini'] ?? 0) as int;
final lastDate = '${data['last_reset_date'] ?? ''}';
if (lastDate != dateKey) {
telurHariIni = 0;
kandang1 = 0;
kandang2 = 0;
}
telurHariIni += panen.jumlahTelur;
totalTelur += panen.jumlahTelur;
final kandangIdLower = panen.kandangId.toLowerCase();
if (kandangIdLower == 'kandang1' || kandangIdLower == 'kandang_1') {
kandang1 += panen.jumlahTelur;
}
if (kandangIdLower == 'kandang2' || kandangIdLower == 'kandang_2') {
kandang2 += panen.jumlahTelur;
}
return Transaction.success({
...data,
'telur_hari_ini': telurHariIni,
'kandang1_hari_ini': kandang1,
'kandang2_hari_ini': kandang2,
'total_telur': totalTelur,
'last_reset_date': dateKey,
'updated_at': DateTime.now().toIso8601String(),
});
});
}
/// Load snapshots dari Firebase ketika app start (untuk sync state)
Future<void> loadTodaySnapshots() async {
try {
final today = DateTime.now();
final todayKey =
'${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}';
final ref = _database.ref('panen_snapshot/$todayKey');
final snapshot = await ref.get();
if (snapshot.exists) {
final data = Map<dynamic, dynamic>.from(snapshot.value as Map);
// Load snapshot pagi
if (data.containsKey('pagi')) {
final pagiData = Map<dynamic, dynamic>.from(data['pagi'] as Map);
pagiData.forEach((kandangId, value) {
if (kandangId != 'timestamp') {
_snapshotPagiHariIni[kandangId] = value as int;
}
});
}
// Load snapshot sore
if (data.containsKey('sore')) {
final soreData = Map<dynamic, dynamic>.from(data['sore'] as Map);
soreData.forEach((kandangId, value) {
if (kandangId != 'timestamp' &&
!kandangId.toString().startsWith('delta_')) {
_snapshotSoreHariIni[kandangId] = value as int;
}
});
}
if (kDebugMode) {
print('✅ Today snapshots loaded from Firebase');
print(' Pagi: $_snapshotPagiHariIni');
print(' Sore: $_snapshotSoreHariIni');
}
}
notifyListeners();
} catch (e) {
if (kDebugMode) {
print('⚠️ Error loading today snapshots: $e');
}
}
}
/// Restore historical panen dari Firebase saat app start
Future<void> restorePanenHistoryFromFirebase() async {
try {
final ref = _database.ref('riwayat/records');
final snapshot = await ref.get();
if (snapshot.exists) {
final data = Map<dynamic, dynamic>.from(snapshot.value as Map);
_panens.clear();
data.forEach((key, value) {
if (value is! Map) return;
final panenData = Map<dynamic, dynamic>.from(value);
final tanggalRaw = panenData['tanggal_panen'];
final tanggal = DateTime.tryParse('${tanggalRaw ?? ''}');
if (tanggal == null) return;
final panen = Panen(
id: panenData['id'] ?? '',
kandangId: panenData['kandang_id'] ?? '',
kandangNama: panenData['kandang_nama'] ?? '',
jumlahTelur: panenData['jumlah_telur'] ?? 0,
tanggalPanen: tanggal,
jam: panenData['jam'] ?? '',
catatan: panenData['catatan'] ?? '',
jenisPanen: panenData['jenis_panen'],
sensorSnapshot: panenData['sensor_snapshot'],
panenSebelumnya: panenData['panen_sebelumnya'],
);
_panens.add(panen);
});
} else {
// Fallback legacy structure: /riwayat langsung berisi records + summary.
final legacySnapshot = await _database.ref('riwayat').get();
if (legacySnapshot.exists) {
final legacyData = Map<dynamic, dynamic>.from(
legacySnapshot.value as Map,
);
_panens.clear();
legacyData.forEach((key, value) {
if (value is! Map) return;
final panenData = Map<dynamic, dynamic>.from(value);
if (!panenData.containsKey('kandang_id')) return;
final tanggalRaw = panenData['tanggal_panen'];
final tanggal = DateTime.tryParse('${tanggalRaw ?? ''}');
if (tanggal == null) return;
_panens.add(
Panen(
id: panenData['id'] ?? '',
kandangId: panenData['kandang_id'] ?? '',
kandangNama: panenData['kandang_nama'] ?? '',
jumlahTelur: panenData['jumlah_telur'] ?? 0,
tanggalPanen: tanggal,
jam: panenData['jam'] ?? '',
catatan: panenData['catatan'] ?? '',
jenisPanen: panenData['jenis_panen'],
sensorSnapshot: panenData['sensor_snapshot'],
panenSebelumnya: panenData['panen_sebelumnya'],
),
);
});
}
_panens.sort((a, b) => b.tanggalPanen.compareTo(a.tanggalPanen));
if (kDebugMode) {
print('✅ Restored ${_panens.length} panen records from Firebase');
}
}
notifyListeners();
} catch (e) {
if (kDebugMode) {
print('⚠️ Error restoring panen history: $e');
}
}
}
@override
void dispose() {
_recordsRealtimeSubscription?.cancel();
super.dispose();
}
}