561 lines
20 KiB
Dart
561 lines
20 KiB
Dart
import 'dart:ui';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:firebase_database/firebase_database.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
|
|
import '../register_page.dart';
|
|
import '../widgets/ui_helpers.dart';
|
|
import 'cards_page.dart';
|
|
import 'reminder_page.dart';
|
|
import 'profile_page.dart';
|
|
import 'history_page.dart';
|
|
import '../login_page.dart';
|
|
|
|
class HomeDashboardPage extends StatefulWidget {
|
|
const HomeDashboardPage({super.key});
|
|
|
|
@override
|
|
State<HomeDashboardPage> createState() => _HomeDashboardPageState();
|
|
}
|
|
|
|
class _HomeDashboardPageState extends State<HomeDashboardPage> {
|
|
final DatabaseReference rtdb = FirebaseDatabase.instance.ref();
|
|
final FirebaseFirestore firestore = FirebaseFirestore.instance;
|
|
final FlutterLocalNotificationsPlugin notif = FlutterLocalNotificationsPlugin();
|
|
|
|
String lastUID = "-";
|
|
String statusAkses = "-";
|
|
String namaUser = "User";
|
|
String emailUser = "-";
|
|
String roleUser = "user";
|
|
|
|
String relayStatus = "OFF";
|
|
String doorSensor = "CLOSED";
|
|
|
|
int reminderHour = 21;
|
|
int reminderMinute = 0;
|
|
bool reminderEnabled = true;
|
|
|
|
bool modeTambah = false;
|
|
String namaKartuBaru = "";
|
|
int selectedIndex = 0;
|
|
String _lastRfidUid = "";
|
|
String _lastRfidStatus = "";
|
|
bool _hasRfidSnapshot = false;
|
|
String _lastLogKey = "";
|
|
DateTime? _lastLogTime;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
initNotification();
|
|
ambilNama();
|
|
listenRfidSistem();
|
|
listenRelay();
|
|
listenDoorSensor();
|
|
listenNotification();
|
|
listenReminder();
|
|
}
|
|
|
|
void initNotification() async {
|
|
const AndroidInitializationSettings android = AndroidInitializationSettings('@mipmap/ic_launcher');
|
|
const InitializationSettings settings = InitializationSettings(android: android);
|
|
await notif.initialize(settings: settings);
|
|
}
|
|
|
|
Future<void> showNotif(String title, String body) async {
|
|
int notifId = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
|
|
"door_security",
|
|
"Door Security",
|
|
importance: Importance.max,
|
|
priority: Priority.high,
|
|
playSound: true,
|
|
enableVibration: true,
|
|
);
|
|
const NotificationDetails details = NotificationDetails(android: androidDetails);
|
|
await notif.show(
|
|
id: notifId,
|
|
title: title,
|
|
body: body,
|
|
notificationDetails: details,
|
|
);
|
|
}
|
|
|
|
Future<void> simpanRiwayatAkses(String uid, String status) async {
|
|
if (uid == "-" || uid.isEmpty || uid == "IDLE" || status == "IDLE") return;
|
|
|
|
final String logKey = '$uid|$status';
|
|
final now = DateTime.now();
|
|
if (_lastLogKey == logKey && _lastLogTime != null && now.difference(_lastLogTime!).inSeconds < 5) {
|
|
debugPrint('Duplicate log skipped: $logKey');
|
|
return;
|
|
}
|
|
|
|
String namaKartu = "Unregistered Card";
|
|
if (uid == "app") {
|
|
namaKartu = namaUser;
|
|
} else if (_isGrantedStatus(status)) {
|
|
namaKartu = "Registered Card (No Name)";
|
|
try {
|
|
final cardDoc = await firestore.collection("cards").doc(uid).get();
|
|
if (cardDoc.exists && cardDoc.data() != null) {
|
|
namaKartu = cardDoc.data()!["name"] ?? "No Name";
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Gagal fetch nama kartu: $e");
|
|
}
|
|
}
|
|
|
|
try {
|
|
await firestore.collection("access_logs").add({
|
|
"uid": uid,
|
|
"name": namaKartu,
|
|
"status": status,
|
|
"timestamp": FieldValue.serverTimestamp(),
|
|
});
|
|
_lastLogKey = logKey;
|
|
_lastLogTime = now;
|
|
debugPrint("Log Sukses Tersimpan di Firestore.");
|
|
} catch (e) {
|
|
debugPrint("Gagal menulis ke Firestore: $e");
|
|
}
|
|
}
|
|
|
|
bool _isGrantedStatus(String status) {
|
|
final s = status.toString().toUpperCase();
|
|
return s.contains('GRANT') || s.contains('SUCCESS') || s.contains('ACCEPT') || s.contains('OK');
|
|
}
|
|
|
|
void listenNotification() {
|
|
rtdb.child("rfid/notification").onValue.listen((event) async {
|
|
if (event.snapshot.value == null) return;
|
|
String notifData = event.snapshot.value.toString();
|
|
|
|
if (notifData == "ACCESS_DENIED") {
|
|
await showNotif("ACCESS DENIED", "RFID card tidak terdaftar");
|
|
await simpanRiwayatAkses(lastUID, "ACCESS_DENIED");
|
|
await rtdb.child("rfid/notification").set("IDLE");
|
|
}
|
|
});
|
|
}
|
|
|
|
void listenReminder() {
|
|
rtdb.child("rfid/reminder").onValue.listen((event) {
|
|
if (event.snapshot.value == null) return;
|
|
Map data = Map.from(event.snapshot.value as Map);
|
|
if (mounted) {
|
|
setState(() {
|
|
reminderEnabled = data["enabled"] ?? true;
|
|
reminderHour = data["hour"] ?? 21;
|
|
reminderMinute = data["minute"] ?? 0;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
Future setReminderTime() async {
|
|
TimeOfDay? picked = await showTimePicker(
|
|
context: context,
|
|
initialTime: TimeOfDay(hour: reminderHour, minute: reminderMinute),
|
|
);
|
|
if (picked != null) {
|
|
await rtdb.child("rfid/reminder").set({
|
|
"enabled": true,
|
|
"hour": picked.hour,
|
|
"minute": picked.minute,
|
|
});
|
|
}
|
|
}
|
|
|
|
Future ambilNama() async {
|
|
final user = FirebaseAuth.instance.currentUser;
|
|
if (user == null) return;
|
|
emailUser = user.email ?? "-";
|
|
final doc = await firestore.collection("users").doc(user.uid).get();
|
|
if (doc.exists && doc.data() != null) {
|
|
if (mounted) {
|
|
setState(() {
|
|
namaUser = doc.data()!["name"] ?? "User";
|
|
roleUser = (doc.data()!["role"] ?? "user").toString();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void listenRfidSistem() {
|
|
rtdb.child("rfid").onValue.listen((event) async {
|
|
if (event.snapshot.value == null) return;
|
|
|
|
final Map<dynamic, dynamic> rfidData = Map.from(event.snapshot.value as Map);
|
|
String uidTerkini = (rfidData["lastScan"] ?? "-").toString();
|
|
String statusTerkini = (rfidData["status"] ?? "-").toString();
|
|
|
|
if (uidTerkini == "IDLE" || statusTerkini == "IDLE" || uidTerkini == "-") {
|
|
return;
|
|
}
|
|
|
|
if (!_hasRfidSnapshot) {
|
|
_lastRfidUid = uidTerkini;
|
|
_lastRfidStatus = statusTerkini;
|
|
_hasRfidSnapshot = true;
|
|
}
|
|
|
|
if (uidTerkini == _lastRfidUid && statusTerkini == _lastRfidStatus) {
|
|
if (mounted) {
|
|
setState(() {
|
|
lastUID = uidTerkini;
|
|
statusAkses = statusTerkini;
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
lastUID = uidTerkini;
|
|
statusAkses = statusTerkini;
|
|
});
|
|
}
|
|
|
|
if (modeTambah) {
|
|
await firestore.collection("cards").doc(uidTerkini).set({
|
|
"name": namaKartuBaru,
|
|
"active": true,
|
|
"createdAt": FieldValue.serverTimestamp()
|
|
});
|
|
await rtdb.child("rfid/cards/$uidTerkini").set({
|
|
"name": namaKartuBaru,
|
|
"active": true,
|
|
});
|
|
if (mounted) {
|
|
setState(() { modeTambah = false; });
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("Card successfully added")),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (statusTerkini == "ACCESS_GRANTED" || statusTerkini == "SUCCESS") {
|
|
final String uidLog = uidTerkini;
|
|
final String statusLog = statusTerkini;
|
|
|
|
await simpanRiwayatAkses(uidLog, statusLog);
|
|
|
|
Future.delayed(const Duration(seconds: 3), () {
|
|
rtdb.child("rfid/status").set("IDLE");
|
|
rtdb.child("rfid/lastScan").set("IDLE");
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
void listenRelay() {
|
|
rtdb.child("rfid/relay").onValue.listen((event) {
|
|
if (event.snapshot.value != null && mounted) {
|
|
setState(() { relayStatus = event.snapshot.value.toString(); });
|
|
}
|
|
});
|
|
}
|
|
|
|
void listenDoorSensor() {
|
|
rtdb.child("rfid/doorSensor").onValue.listen((event) {
|
|
if (event.snapshot.value != null && mounted) {
|
|
setState(() { doorSensor = event.snapshot.value.toString(); });
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> bukaPintu() async {
|
|
await rtdb.child("rfid/doorControl").set("OPEN");
|
|
await simpanRiwayatAkses("app", "APP_OPEN");
|
|
}
|
|
|
|
Future<void> tutupPintu() async {
|
|
await rtdb.child("rfid/doorControl").set("CLOSE");
|
|
await simpanRiwayatAkses("app", "APP_CLOSE");
|
|
}
|
|
|
|
Future hapusKartu(String uid) async {
|
|
await firestore.collection("cards").doc(uid).delete();
|
|
await rtdb.child("rfid/cards/$uid").remove();
|
|
}
|
|
|
|
bool get isAdmin => roleUser.toLowerCase() == 'admin';
|
|
|
|
Future<void> openCreateUserPage() async {
|
|
if (!isAdmin) return;
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => const RegisterPage(role: 'user', signOutAfterRegister: false),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future logout() async {
|
|
await FirebaseAuth.instance.signOut();
|
|
if (mounted) {
|
|
Navigator.pushAndRemoveUntil(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const LoginPage()),
|
|
(route) => false,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> tambahKartuDialog() async {
|
|
if (!isAdmin) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("Access denied. Only admin can add cards."), backgroundColor: Colors.redAccent),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final snapshot = await firestore.collection("cards").get();
|
|
if (snapshot.docs.length >= 10) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("Cannot add card. Maximum limit reached!"), backgroundColor: Colors.redAccent),
|
|
);
|
|
return;
|
|
}
|
|
|
|
TextEditingController controller = TextEditingController();
|
|
if (!mounted) return;
|
|
showDialog(
|
|
context: context,
|
|
builder: (_) => AlertDialog(
|
|
backgroundColor: const Color(0xff111111),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
|
|
title: const Text("Add Card", style: TextStyle(color: Colors.white)),
|
|
content: TextField(
|
|
controller: controller,
|
|
style: const TextStyle(color: Colors.white),
|
|
decoration: InputDecoration(
|
|
hintText: "Card Name",
|
|
hintStyle: const TextStyle(color: Colors.white38),
|
|
filled: true,
|
|
fillColor: Colors.black,
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(18), borderSide: BorderSide.none),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text("Cancel")),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blueAccent,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
|
),
|
|
onPressed: () {
|
|
if (controller.text.isNotEmpty) {
|
|
namaKartuBaru = controller.text;
|
|
setState(() { modeTambah = true; });
|
|
Navigator.pop(context);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("Please scan RFID card")),
|
|
);
|
|
}
|
|
},
|
|
child: const Text("Scan"),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
bool relayOn = relayStatus == "ON";
|
|
bool doorOpen = doorSensor == "OPEN";
|
|
|
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
|
value: const SystemUiOverlayStyle(
|
|
statusBarColor: Colors.transparent,
|
|
statusBarIconBrightness: Brightness.light,
|
|
),
|
|
child: Scaffold(
|
|
backgroundColor: Colors.black,
|
|
floatingActionButton: selectedIndex == 1 && isAdmin
|
|
? FloatingActionButton.extended(
|
|
backgroundColor: Colors.blueAccent,
|
|
elevation: 10,
|
|
onPressed: tambahKartuDialog,
|
|
icon: const Icon(Icons.add),
|
|
label: const Text("Add Card"),
|
|
)
|
|
: null,
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), // Dipersempit agar pas di Pova 5
|
|
child: IndexedStack(
|
|
index: selectedIndex,
|
|
children: [
|
|
// TAB 0: DASHBOARD (UKURAN LEBIH COMPACT)
|
|
SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
modernHeader(),
|
|
const SizedBox(height: 20), // Jarak dikurangi
|
|
ModernCard(
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
height: 100, width: 100, // Diperkecil dari 130 ke 100
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: LinearGradient(
|
|
colors: doorOpen ? [Colors.greenAccent, Colors.green] : [Colors.redAccent, Colors.red],
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: doorOpen ? Colors.green.withOpacity(0.4) : Colors.red.withOpacity(0.4),
|
|
blurRadius: 18,
|
|
),
|
|
],
|
|
),
|
|
child: Icon(
|
|
doorOpen ? Icons.lock_open_rounded : Icons.lock_rounded,
|
|
color: Colors.white, size: 50, // Ikon diperkecil dari 70 ke 50
|
|
),
|
|
),
|
|
const SizedBox(height: 15),
|
|
Text(
|
|
doorOpen ? 'DOOR OPEN' : 'DOOR LOCKED',
|
|
style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold), // Font diperkecil
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
relayOn ? 'Security system active' : 'Security system standby',
|
|
style: const TextStyle(color: Colors.white54, fontSize: 13),
|
|
),
|
|
const SizedBox(height: 15),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), // Padding switch diperkecil
|
|
decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(50)),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text('Door Control', style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)),
|
|
const SizedBox(width: 10),
|
|
Transform.scale(
|
|
scale: 0.85, // Switch diperkecil sedikit agar presisi
|
|
child: Switch(
|
|
value: relayOn,
|
|
activeColor: Colors.greenAccent,
|
|
onChanged: (value) { value ? bukaPintu() : tutupPintu(); },
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 15),
|
|
ModernCard(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.history, color: Colors.white, size: 20),
|
|
SizedBox(width: 8),
|
|
Text('LAST SCAN STATE', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
const SizedBox(height: 15),
|
|
InfoTile(icon: Icons.badge_rounded, title: 'UID CARD', value: lastUID),
|
|
const SizedBox(height: 10),
|
|
InfoTile(icon: Icons.security_rounded, title: 'STATUS', value: statusAkses),
|
|
const SizedBox(height: 10),
|
|
InfoTile(
|
|
icon: Icons.access_time_rounded,
|
|
title: 'TIME',
|
|
value: '${DateTime.now().day.toString().padLeft(2, '0')}/${DateTime.now().month.toString().padLeft(2, '0')}/${DateTime.now().year} ${TimeOfDay.now().format(context)}',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
CardsPage(firestore: firestore, onDeleteCard: hapusKartu, canEditCards: isAdmin),
|
|
HistoryPage(firestore: firestore, canClearHistory: isAdmin),
|
|
ReminderPage(
|
|
reminderEnabled: reminderEnabled,
|
|
reminderHour: reminderHour,
|
|
reminderMinute: reminderMinute,
|
|
onSetReminderTime: setReminderTime,
|
|
onToggleReminder: (value) { rtdb.child('rfid/reminder/enabled').set(value); },
|
|
),
|
|
ProfilePage(
|
|
namaUser: namaUser, emailUser: emailUser, role: roleUser,
|
|
onLogout: logout, onCreateUser: isAdmin ? openCreateUserPage : null,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
bottomNavigationBar: Container(
|
|
margin: const EdgeInsets.only(left: 12, right: 12, bottom: 12, top: 4), // Dikurangi margin bawahnya
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xff111111),
|
|
borderRadius: BorderRadius.circular(24),
|
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 15)],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(24),
|
|
child: BottomNavigationBar(
|
|
currentIndex: selectedIndex,
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
selectedItemColor: Colors.blueAccent,
|
|
unselectedItemColor: Colors.white38,
|
|
type: BottomNavigationBarType.fixed,
|
|
iconSize: 22, // Ukuran ikon navigasi diturunkan sedikit agar proporsional
|
|
selectedFontSize: 11,
|
|
unselectedFontSize: 11,
|
|
onTap: (index) { setState(() { selectedIndex = index; }); },
|
|
items: const [
|
|
BottomNavigationBarItem(icon: Icon(Icons.home_rounded), label: "Home"),
|
|
BottomNavigationBarItem(icon: Icon(Icons.credit_card_rounded), label: "Cards"),
|
|
BottomNavigationBarItem(icon: Icon(Icons.manage_search_rounded), label: "History"),
|
|
BottomNavigationBarItem(icon: Icon(Icons.notifications_active_rounded), label: "Reminder"),
|
|
BottomNavigationBarItem(icon: Icon(Icons.person_rounded), label: "Profile"),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget modernHeader() {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('DOORGUARD', style: TextStyle(color: Colors.white, fontSize: 26, fontWeight: FontWeight.bold, letterSpacing: 0.5)), // Ukuran font disesuaikan
|
|
SizedBox(height: 4),
|
|
Text('Smart RFID Door Monitoring System', style: TextStyle(color: Colors.white54, fontSize: 13)),
|
|
],
|
|
),
|
|
GestureDetector(
|
|
onTap: () { setState(() { selectedIndex = 4; }); },
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(color: const Color(0xff111111), borderRadius: BorderRadius.circular(15)),
|
|
child: const Icon(Icons.person, color: Colors.white, size: 22),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
} |