import 'dart:async'; import 'package:flutter/material.dart'; import 'package:firebase_database/firebase_database.dart'; import '../widgets/fade_in_up.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; // ================= TOP LEVEL HANDLER ================= @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { print("Handling background message: ${message.messageId}"); } final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); // ================= MODEL ================= class NotificationModel { final String id; final String time; final String date; final double suhu; final double alkohol; final String status; final String tipe; final String jenisTape; final String dbPath; NotificationModel({ required this.id, required this.time, required this.date, required this.suhu, required this.alkohol, required this.status, required this.tipe, required this.jenisTape, required this.dbPath, }); } class NotificationPage extends StatefulWidget { const NotificationPage({super.key}); @override State createState() => _NotificationPageState(); } class _NotificationPageState extends State { final DatabaseReference notifRef = FirebaseDatabase.instance.ref("notifikasi"); final DatabaseReference warningRef = FirebaseDatabase.instance.ref("peringatan"); List notifications = []; bool isLoading = true; String selectedFilter = "Monitoring"; final Map _notifMap = {}; final Map _warningMap = {}; @override void initState() { super.initState(); initLocalNotif(); initFCM(); listenNotification(); } @override void dispose() { super.dispose(); } void _deleteNotification(NotificationModel n) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text("Hapus Pesan?"), content: const Text("Apakah Anda yakin ingin menghapus pesan ini?"), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text("Batal")), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () { FirebaseDatabase.instance.ref(n.dbPath).child(n.id).remove(); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Pesan berhasil dihapus")), ); }, child: const Text("Hapus", style: TextStyle(color: Colors.white)), ), ], ), ); } void initLocalNotif() async { const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/launcher_icon'); const InitializationSettings settings = InitializationSettings( android: androidSettings, ); await flutterLocalNotificationsPlugin.initialize(settings); } void initFCM() async { FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); await FirebaseMessaging.instance.subscribeToTopic("fermentasi_update"); FirebaseMessaging.onMessage.listen((RemoteMessage message) { if (message.notification != null) { String body = message.notification!.body ?? ""; body = body .replaceAll(RegExp(r'Proses Optimal', caseSensitive: false), 'Belum Matang') .replaceAll(RegExp(r'Proses Awal', caseSensitive: false), 'Belum Matang') .replaceAll(RegExp(r'Menjelang Matang', caseSensitive: false), 'Belum Matang'); flutterLocalNotificationsPlugin.show( message.hashCode, message.notification!.title, body, const NotificationDetails( android: AndroidNotificationDetails( 'fermentasi_channel', 'Fermentasi Notifikasi', importance: Importance.max, priority: Priority.high, icon: '@mipmap/launcher_icon', ), ), ); } }); } NotificationModel? _parseNotification(String key, dynamic value, String sourcePath) { try { if (value is! Map) return null; String id = key.toString(); DateTime notifDate = DateTime.now(); // Ekstrak waktu dari Firebase Push ID untuk akurasi tinggi if (id.startsWith('-') && id.length >= 8) { const chars = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'; int time = 0; for (int i = 0; i < 8; i++) { int index = chars.indexOf(id[i]); if (index != -1) { time = time * 64 + index; } } if (time > 1000000000000) { // Validasi tahun (lebih dari 2001) notifDate = DateTime.fromMillisecondsSinceEpoch(time); } } String datePart = "${notifDate.day.toString().padLeft(2, '0')}-${notifDate.month.toString().padLeft(2, '0')}-${notifDate.year}"; String timePart = "${notifDate.hour.toString().padLeft(2, '0')}:${notifDate.minute.toString().padLeft(2, '0')}"; // Ekstrak tipe dengan sangat kokoh (mendukung key "tipe" / "type" dan status bahaya/warning/terlalu matang) String rawTipe = (value["tipe"] ?? value["type"] ?? "").toString().toLowerCase().trim(); String rawStatus = (value["status"] ?? "").toString().toLowerCase().trim(); String tipeStr; if (sourcePath == "peringatan") { tipeStr = "Peringatan"; } else if (rawTipe == "selesai" || rawTipe == "done" || rawTipe == "finished" || rawTipe == "complete" || rawStatus == "matang" || rawStatus == "selesai") { tipeStr = "Selesai"; } else if (rawTipe == "peringatan" || rawTipe == "warning" || rawTipe == "alert" || rawTipe == "danger" || rawTipe == "bahaya" || rawStatus.contains("peringatan") || rawStatus.contains("terlalu matang") || rawStatus.contains("bahaya") || rawStatus.contains("warning") || rawStatus.contains("alert") || rawStatus.contains("danger") || rawStatus.contains("tinggi")) { tipeStr = "Peringatan"; } else { tipeStr = "Monitoring"; } if (tipeStr == "Selesai") { String total = value["jam_total"]?.toString() ?? value["waktu"]?.toString() ?? ""; if (total.isNotEmpty && !total.contains("/")) { if (total.toLowerCase().contains("jam")) { timePart = total; } else { double? val = double.tryParse(total); if (val != null) { if (val == 0) { timePart = "0 Detik"; } else if (val < 0.016) { // Kurang dari ~1 Menit timePart = "${(val * 3600).toInt()} Detik"; } else if (val < 1.0) { // Kurang dari 1 Jam timePart = "${(val * 60).toInt()} Menit"; } else { timePart = "${val.toStringAsFixed(2)} Jam"; } } else { timePart = "$total Jam"; } } } } String statusStr = value["status"]?.toString() ?? (sourcePath == "peringatan" ? "Terlalu Matang" : "Monitoring"); String lowerStatus = statusStr.toLowerCase(); if (lowerStatus.contains("optimal") || lowerStatus.contains("awal") || lowerStatus.contains("menjelang matang")) { statusStr = "Belum Matang"; } if (tipeStr == "Peringatan" && !statusStr.toLowerCase().contains("terlalu matang")) { return null; } return NotificationModel( id: id, time: timePart, date: datePart, suhu: (value["suhu"] as num?)?.toDouble() ?? 0.0, alkohol: (value["alkohol"] as num?)?.toDouble() ?? 0.0, status: statusStr, tipe: tipeStr, jenisTape: value["jenisTape"] ?? "Tape Ketan", dbPath: sourcePath, ); } catch (e) { debugPrint("Error parsing single notification: $e"); return null; } } void listenNotification() { // 1. Listen to 'notifikasi' path notifRef.onValue.listen((event) { final data = event.snapshot.value as Map?; _notifMap.clear(); if (data != null) { data.forEach((key, value) { final model = _parseNotification(key.toString(), value, "notifikasi"); if (model != null) { _notifMap[model.id] = model; } }); } _combineAndSortNotifications(); }); // 2. Listen to 'peringatan' path warningRef.onValue.listen((event) { final data = event.snapshot.value as Map?; _warningMap.clear(); if (data != null) { data.forEach((key, value) { final model = _parseNotification(key.toString(), value, "peringatan"); if (model != null) { _warningMap[model.id] = model; } }); } _combineAndSortNotifications(); }); } void _combineAndSortNotifications() { List temp = [..._notifMap.values, ..._warningMap.values]; temp.sort((a, b) => b.id.compareTo(a.id)); // Reversed push-ID (newest first) if (mounted) { setState(() { notifications = temp; isLoading = false; }); } } @override Widget build(BuildContext context) { return Scaffold( extendBody: true, body: Stack( children: [ Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, stops: [0.0, 0.4, 0.7, 1.0], colors: [ Color(0xFFE8F5E9), Color(0xFFFFFDF0), Color(0xFFF1F8E9), Color(0xFFE8F5E9), ], ), ), ), Column( children: [ FadeInUp(delay: 0, child: _buildAppBar()), FadeInUp(delay: 150, child: _buildFilterTabs()), Expanded( child: isLoading ? const Center(child: CircularProgressIndicator()) : FadeInUp(delay: 300, child: _buildMessageList()), ), ], ), ], ), ); } // ================= 1. APP BAR ================= Widget _buildAppBar() { return SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 24, 20, 5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon(Icons.arrow_back_ios_new, size: 20), onPressed: () => Navigator.pop(context), ), const Expanded( child: Text( "Fermentation Message", textAlign: TextAlign.center, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const Icon(Icons.notifications_active, color: Color(0xFF2E7D32)), ], ), ), ); } // ================= 2. FILTER TABS ================= Widget _buildFilterTabs() { return Padding( padding: const EdgeInsets.only(top: 18, bottom: 18), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _filterBtn("Monitoring"), _filterBtn("Peringatan"), _filterBtn("Selesai"), ], ), ); } Widget _filterBtn(String label) { bool isSelected = selectedFilter == label; return GestureDetector( onTap: () => setState(() => selectedFilter = label), child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: BoxDecoration( color: isSelected ? const Color(0xFF2E7D32) : const Color(0xFFE8F5E9), borderRadius: BorderRadius.circular(15), gradient: isSelected ? const LinearGradient( colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)], ) : null, ), child: Text( label, style: TextStyle( color: isSelected ? Colors.white : const Color(0xFF2E7D32), fontWeight: FontWeight.bold, ), ), ), ); } // ================= 3. LIST PESAN ================= Widget _buildMessageList() { final filteredList = notifications .where((n) => n.tipe.toLowerCase() == selectedFilter.toLowerCase()) .toList(); if (filteredList.isEmpty) { return const Center(child: Text("Belum ada pesan untuk kategori ini")); } return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 20), itemCount: filteredList.length, itemBuilder: (context, index) { final n = filteredList[index]; return _buildMessageCard(n); }, ); } Widget _buildMessageCard(NotificationModel n) { bool isSelesai = n.tipe.toLowerCase() == "selesai"; bool isManual = n.tipe.toLowerCase() == "peringatan"; return Container( margin: const EdgeInsets.only(bottom: 18), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(25), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 15, offset: const Offset(0, 5), ), ], border: Border.all(color: Colors.grey.withOpacity(0.1)), ), child: Stack( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "${n.jenisTape} | ${n.tipe}", style: const TextStyle(color: Colors.black45, fontSize: 13), ), const SizedBox(height: 8), if (isManual) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "${n.jenisTape} Terlalu Matang", style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, height: 1.2, color: Colors.redAccent, ), ), const SizedBox(height: 4), Text( "Alkohol saat ini: ${n.jenisTape.contains('Singkong') ? n.alkohol.toStringAsFixed(1) : n.alkohol.toStringAsFixed(2)}%", style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ], ) else Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Suhu: ${n.suhu}°C", style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), Text( "Alkohol: ${n.jenisTape.contains('Singkong') ? n.alkohol.toStringAsFixed(1) : n.alkohol.toStringAsFixed(2)}%", style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), Text( "Status: ${n.status}", style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 12), if (!isManual) Row( children: [ const Icon( Icons.access_time_filled, size: 16, color: Color(0xFF81C784), ), const SizedBox(width: 5), Text( n.time, style: const TextStyle( color: Color(0xFF81C784), fontWeight: FontWeight.w600, ), ), const SizedBox(width: 15), const Icon( Icons.calendar_month, size: 16, color: Color(0xFF81C784), ), const SizedBox(width: 5), Text( n.date, style: const TextStyle( color: Color(0xFF81C784), fontWeight: FontWeight.w600, ), ), ], ), ], ), Positioned( right: 0, top: 0, child: GestureDetector( onTap: () => _deleteNotification(n), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: const Icon( Icons.delete_outline_rounded, color: Colors.red, size: 18, ), ), ), ), if (!isManual) Positioned( right: 0, bottom: 0, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: isSelesai ? const Color(0xFFE8F5E9) : const Color(0xFFFFFDE7), borderRadius: BorderRadius.circular(8), ), child: Text( isSelesai ? "Done" : "In Progress", style: TextStyle( color: isSelesai ? const Color(0xFF2E7D32) : Colors.orange, fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ), ], ), ); } }