TKK_E32230499/lib/screen/dashboard.dart

927 lines
29 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_database/firebase_database.dart';
import 'history.dart';
import 'pesan.dart';
import 'akun.dart';
import '../widgets/fade_in_up.dart';
class DashboardPage extends StatefulWidget {
const DashboardPage({super.key});
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
final user = FirebaseAuth.instance.currentUser;
final sensorRef = FirebaseDatabase.instance.ref("fermentasi");
final controlRef = FirebaseDatabase.instance.ref("control");
double suhu = 0;
double alkohol = 0;
double waktu = 0;
String status = "Belum Mulai";
bool autoMode = true;
bool ptcOn = false;
bool kipas1On = false;
bool kipas2On = false;
bool fermentasiAktif = false;
String selectedTape = "";
Timer? _timer;
int detikBerjalan = 0;
int waktuMulaiUi = 0;
double _swipeValue = 0.0;
@override
void initState() {
super.initState();
listenSensor();
listenControl();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (fermentasiAktif && mounted) {
setState(() {
detikBerjalan++;
});
}
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void updateStatusKematangan() {
if (autoMode) {
if (!fermentasiAktif) {
// Cek threshold bahkan saat belum mulai di Mode Otomatis
if (selectedTape == "Ketan") {
if (alkohol >= 0.32) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 0.25) {
status = "Matang";
return;
}
} else if (selectedTape == "Singkong") {
if (alkohol > 5.8) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 3.5) {
status = "Matang";
return;
}
}
status = "Belum Mulai";
return;
}
// Di mode otomatis, gunakan treshold manual sebagai pendeteksi langsung di UI
if (selectedTape == "Ketan") {
if (alkohol >= 0.32) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 0.25) {
status = "Matang";
return;
}
} else if (selectedTape == "Singkong") {
if (alkohol > 5.8) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 3.5) {
status = "Matang";
return;
}
}
// In autoMode, the status is determined by the ESP32 dynamic peak-detection state machine
// and uploaded to Firebase (/fermentasi/status), which is handled in listenSensor().
// If no status has been loaded from Firebase yet, we set a sensible default.
if (status == "Belum Mulai") {
status = "Belum Matang";
}
} else {
// Mode Manual (matches the ESP32 manual thresholds)
if (selectedTape == "Ketan") {
if (alkohol >= 0.32) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 0.25) {
status = "Matang";
return;
} else {
status = "Belum Matang";
}
} else if (selectedTape == "Singkong") {
if (alkohol > 5.8) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 3.5) {
status = "Matang";
return;
} else {
status = "Belum Matang";
}
}
if (!fermentasiAktif) {
status = "Belum Mulai";
}
}
}
void listenSensor() {
sensorRef.onValue.listen((event) {
final data = event.snapshot.value as Map?;
if (data != null) {
if (mounted) {
setState(() {
suhu = (data["suhu"] as num?)?.toDouble() ?? 0;
alkohol = (data["alkohol"] as num?)?.toDouble() ?? 0;
if (data["waktu"] != null) {
waktu = (data["waktu"] as num?)?.toDouble() ?? 0;
int espSeconds = (waktu * 3600).toInt();
if (fermentasiAktif) {
if ((detikBerjalan - espSeconds).abs() > 10) {
detikBerjalan = espSeconds;
}
} else {
detikBerjalan = espSeconds;
}
}
if (data["status"] != null) {
String rawStatus = data["status"].toString();
String lowerStatus = rawStatus.toLowerCase();
// Standardize intermediate phases to "Belum Matang" to match notifications & history page mapping
if (lowerStatus.contains("optimal") || lowerStatus.contains("awal") || lowerStatus.contains("menjelang matang")) {
status = "Belum Matang";
} else {
status = rawStatus;
}
} else {
updateStatusKematangan();
}
});
}
}
});
}
void listenControl() {
controlRef.onValue.listen((event) {
final data = event.snapshot.value as Map?;
if (data != null) {
if (mounted) {
setState(() {
autoMode = data["autoMode"] ?? true;
ptcOn = data["ptc"] ?? false;
kipas1On = data["kipas1"] ?? false; // kipas1On (pemanas) baca dari "kipas1"
kipas2On = data["kipas2"] ?? false; // kipas2On (kipas pendingin) baca dari "kipas2"
fermentasiAktif = data["fermentasi_aktif"] ?? false;
waktuMulaiUi = data["waktu_mulai_ui"] ?? 0;
if (fermentasiAktif && waktuMulaiUi > 0) {
int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
int diff = now - waktuMulaiUi;
if (diff >= 0 && (detikBerjalan - diff).abs() > 2) {
detikBerjalan = diff;
}
}
if (data["jenisTape"] != null) {
selectedTape = data["jenisTape"];
}
bool resetReq = data["reset_request"] ?? false;
if (resetReq) {
status = "Belum Mulai";
detikBerjalan = 0;
waktu = 0;
}
// Only update status locally if in manual mode or if fermentation is stopped
if (!autoMode || !fermentasiAktif) {
updateStatusKematangan();
}
});
}
}
});
}
String formatWaktu(double jam) {
int total = (jam * 3600).toInt();
int h = total ~/ 3600;
int m = (total % 3600) ~/ 60;
int s = total % 60;
return "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}";
}
String formatWaktuDetik(int totalSeconds) {
int h = totalSeconds ~/ 3600;
int m = (totalSeconds % 3600) ~/ 60;
int s = totalSeconds % 60;
return "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}";
}
String getUsername() {
if (user?.displayName != null && user!.displayName!.isNotEmpty)
return user!.displayName!;
if (user?.email != null) return user!.email!.split("@")[0];
return "User";
}
@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),
],
),
),
),
SafeArea(
bottom: false,
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 110),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FadeInUp(delay: 0, child: _header()),
const SizedBox(height: 28),
FadeInUp(delay: 150, child: _startFermentation()),
const SizedBox(height: 28),
FadeInUp(delay: 300, child: _actuatorControl()),
const SizedBox(height: 20),
],
),
),
),
],
),
bottomNavigationBar: _buildBottomNavbar(),
);
}
Widget _buildBottomNavbar() {
return Container(
decoration: const BoxDecoration(
color: Color(0xFFE8F5E9),
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, -2),
),
],
),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
child: BottomNavigationBar(
currentIndex: 0,
onTap: (index) {
if (index == 1) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HistoryPage()),
);
} else if (index == 2) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AkunScreen()),
);
}
},
showSelectedLabels: false,
showUnselectedLabels: false,
backgroundColor: Colors.transparent,
elevation: 0,
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: _navIcon(Icons.home_filled, 0),
label: 'Home',
),
BottomNavigationBarItem(
icon: _navIcon(Icons.history, 1),
label: 'History',
),
BottomNavigationBarItem(
icon: _navIcon(Icons.people_alt_rounded, 2),
label: 'Profile',
),
],
),
),
);
}
Widget _navIcon(IconData icon, int index) {
bool isSelected = index == 0;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: isSelected
? const LinearGradient(
colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)],
)
: null,
),
child: Icon(
icon,
color: isSelected ? Colors.white : const Color(0xFF81C784),
size: 24,
),
);
}
Widget _header() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Hello!",
style: TextStyle(color: Colors.black54, fontSize: 13),
),
Text(
getUsername(),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
const Spacer(),
GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NotificationPage(),
),
),
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: const Color(0xFF2E7D32),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.notifications_none_rounded,
color: Colors.white,
size: 22,
),
),
),
],
),
const SizedBox(height: 20),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 22),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)],
),
borderRadius: BorderRadius.circular(28),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Fermentasi kamu hari ini!",
style: TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Text(
status,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_chip("Alkohol ${selectedTape == 'Singkong' ? alkohol.toStringAsFixed(1) : alkohol.toStringAsFixed(2)}%"),
const SizedBox(height: 8),
_chip("Suhu ${suhu.toStringAsFixed(1)}°C"),
const SizedBox(height: 8),
_chip(
fermentasiAktif
? formatWaktuDetik(detikBerjalan)
: (status != "Belum Mulai" ? formatWaktu(waktu) : "00:00:00"),
mono: true,
),
],
),
],
),
),
],
);
}
Widget _chip(String text, {bool mono = false}) {
return Container(
width: 130,
padding: const EdgeInsets.symmetric(vertical: 9),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
),
child: Center(
child: Text(
text,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12.5,
fontFamily: mono ? 'monospace' : null,
),
),
),
);
}
Widget _startFermentation() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Start Fermentation",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
if (fermentasiAktif)
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF2E7D32).withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFF2E7D32),
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
const Text(
"Running...",
style: TextStyle(
color: Color(0xFF2E7D32),
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
],
),
const SizedBox(height: 14),
_segmentedModeSelector(),
const SizedBox(height: 16),
_simpleStartButton(),
],
);
}
Widget _segmentedModeSelector() {
bool isKetan = selectedTape == "Ketan";
bool isSingkong = selectedTape == "Singkong";
bool hasSelection = selectedTape.isNotEmpty;
return Container(
height: 52,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: const Color(0xFFE8F5E9),
borderRadius: BorderRadius.circular(26),
),
child: LayoutBuilder(
builder: (context, constraints) {
double totalWidth = constraints.maxWidth;
if (totalWidth.isInfinite || totalWidth <= 0) {
totalWidth = MediaQuery.of(context).size.width - 40;
}
double width = totalWidth / 2;
return Stack(
children: [
AnimatedAlign(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
alignment: isKetan ? Alignment.centerLeft : (isSingkong ? Alignment.centerRight : Alignment.center),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: hasSelection ? 1.0 : 0.0,
child: Container(
width: width,
height: double.infinity,
decoration: BoxDecoration(
color: isKetan ? const Color(0xFF2E7D32) : const Color(0xFFF9A825),
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(
color: (isKetan ? const Color(0xFF2E7D32) : const Color(0xFFF9A825))
.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
),
),
),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
if (!fermentasiAktif) {
setState(() => selectedTape = "Ketan");
controlRef.update({"jenisTape": "Ketan"});
}
},
behavior: HitTestBehavior.opaque,
child: Center(
child: Text(
"Tape Ketan",
style: TextStyle(
fontWeight: FontWeight.bold,
color: isKetan ? Colors.white : Colors.black54,
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
if (!fermentasiAktif) {
setState(() => selectedTape = "Singkong");
controlRef.update({"jenisTape": "Singkong"});
}
},
behavior: HitTestBehavior.opaque,
child: Center(
child: Text(
"Tape Singkong",
style: TextStyle(
fontWeight: FontWeight.bold,
color: isSingkong ? Colors.white : Colors.black54,
),
),
),
),
),
],
),
],
);
},
),
);
}
Widget _simpleStartButton() {
Color btnColor = fermentasiAktif ? Colors.redAccent : const Color(0xFF2E7D32);
return Container(
width: double.infinity,
height: 54,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: btnColor.withOpacity(0.25),
blurRadius: 15,
offset: const Offset(0, 6),
),
],
),
child: ElevatedButton.icon(
onPressed: _toggleFermentation,
style: ElevatedButton.styleFrom(
backgroundColor: btnColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 0,
),
icon: Icon(
fermentasiAktif ? Icons.stop_circle_rounded : Icons.play_circle_filled_rounded,
size: 24,
),
label: Text(
fermentasiAktif ? "STOP FERMENTASI" : "MULAI FERMENTASI",
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
),
);
}
void _toggleFermentation() {
if (!fermentasiAktif && selectedTape.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Pilih jenis tape terlebih dahulu!"),
backgroundColor: Colors.redAccent,
),
);
return;
}
int nowUnix = DateTime.now().millisecondsSinceEpoch ~/ 1000;
Map<String, dynamic> updates = {
"fermentasi_aktif": !fermentasiAktif,
"jenisTape": selectedTape,
};
if (!fermentasiAktif) {
updates["reset_request"] = true;
updates["waktu_mulai_ui"] = nowUnix;
}
controlRef.update(updates);
if (!fermentasiAktif) {
setState(() {
detikBerjalan = 0;
waktuMulaiUi = nowUnix;
});
}
}
Widget _actuatorControl() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Actuator Control",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF2D3142),
),
),
const SizedBox(height: 16),
Column(
children: [
Row(
children: [
Expanded(
child: _buildActuatorCard(
title: "Kipas Pemanas",
subtitle: "Heater Fan",
isActive: kipas1On,
icon: Icons.toys_outlined,
activeGradient: const [Color(0xFFFFB74D), Color(0xFFF57C00)],
onTap: () {
if (autoMode) {
_showAutoModeWarning();
} else {
controlRef.update({"kipas1": !kipas1On});
}
},
isLocked: autoMode,
),
),
const SizedBox(width: 14),
Expanded(
child: _buildActuatorCard(
title: "PTC Heater",
subtitle: "Pemanas PTC",
isActive: ptcOn,
icon: Icons.local_fire_department_outlined,
activeGradient: const [Color(0xFFFF8A65), Color(0xFFD84315)],
onTap: () {
if (autoMode) {
_showAutoModeWarning();
} else {
controlRef.update({"ptc": !ptcOn});
}
},
isLocked: autoMode,
),
),
],
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: _buildActuatorCard(
title: "Kipas Pendingin",
subtitle: "Cooler Fan",
isActive: kipas2On,
icon: Icons.ac_unit_rounded,
activeGradient: const [Color(0xFF0288D1), Color(0xFF03A9F4)],
onTap: () {
if (autoMode) {
_showAutoModeWarning();
} else {
controlRef.update({"kipas2": !kipas2On});
}
},
isLocked: autoMode,
),
),
const SizedBox(width: 14),
Expanded(
child: _buildActuatorCard(
title: "Mode Otomatis",
subtitle: "Smart Auto",
isActive: autoMode,
icon: Icons.settings_suggest_rounded,
activeGradient: const [Color(0xFF2E7D32), Color(0xFF4CAF50)],
onTap: () {
controlRef.update({"autoMode": !autoMode});
},
isLocked: !autoMode,
),
),
],
),
],
),
],
);
}
void _showAutoModeWarning() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Nonaktifkan Mode Otomatis terlebih dahulu!"),
backgroundColor: Colors.redAccent,
duration: Duration(seconds: 2),
),
);
}
Widget _buildActuatorCard({
required String title,
required String subtitle,
required bool isActive,
required IconData icon,
required List<Color> activeGradient,
required VoidCallback onTap,
required bool isLocked,
}) {
Color cardBg = Colors.white;
Color borderCol = isActive ? activeGradient[0] : Colors.grey.withOpacity(0.08);
double borderWidth = isActive ? 1.5 : 1.0;
Color iconBg = isActive ? activeGradient[0] : const Color(0xFFF5F7F8);
Color iconCol = isActive ? Colors.white : Colors.black38;
Color titleCol = Colors.black87;
Color subtitleCol = isActive ? activeGradient[0].withOpacity(0.8) : Colors.black38;
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cardBg,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: borderCol,
width: borderWidth,
),
boxShadow: [
if (isActive)
BoxShadow(
color: activeGradient[0].withOpacity(0.12),
blurRadius: 16,
offset: const Offset(0, 8),
)
else
BoxShadow(
color: Colors.black.withOpacity(0.01),
blurRadius: 6,
offset: const Offset(0, 3),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Glowing Icon Wrapper
AnimatedContainer(
duration: const Duration(milliseconds: 250),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: iconBg,
shape: BoxShape.circle,
),
child: Icon(
icon,
color: iconCol,
size: 22,
),
),
// Lock Icon or custom Switch Toggle
isLocked
? const Icon(
Icons.lock_outline_rounded,
color: Colors.black26,
size: 20,
)
: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: 36,
height: 20,
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: isActive ? activeGradient[0].withOpacity(0.15) : Colors.black.withOpacity(0.08),
borderRadius: BorderRadius.circular(10),
),
child: AnimatedAlign(
duration: const Duration(milliseconds: 200),
alignment: isActive ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: isActive ? activeGradient[0] : Colors.black26,
shape: BoxShape.circle,
),
),
),
),
],
),
const SizedBox(height: 18),
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: titleCol,
),
),
const SizedBox(height: 3),
Text(
subtitle,
style: TextStyle(
fontSize: 11,
color: subtitleCol,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
}