TKK_E32230499/lib/screen/pesan.dart

564 lines
19 KiB
Dart

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<void> _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<NotificationPage> createState() => _NotificationPageState();
}
class _NotificationPageState extends State<NotificationPage> {
final DatabaseReference notifRef = FirebaseDatabase.instance.ref("notifikasi");
final DatabaseReference warningRef = FirebaseDatabase.instance.ref("peringatan");
List<NotificationModel> notifications = [];
bool isLoading = true;
String selectedFilter = "Monitoring";
final Map<String, NotificationModel> _notifMap = {};
final Map<String, NotificationModel> _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<dynamic, dynamic>?;
_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<dynamic, dynamic>?;
_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<NotificationModel> 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,
),
),
),
),
],
),
);
}
}