517 lines
16 KiB
Dart
517 lines
16 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:firebase_database/firebase_database.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:intl/date_symbol_data_local.dart';
|
|
import 'dashboard.dart';
|
|
import 'akun.dart';
|
|
import '../widgets/fade_in_up.dart';
|
|
|
|
class HistoryModel {
|
|
final String key;
|
|
final String time;
|
|
final String date;
|
|
final double suhu;
|
|
final double alkohol;
|
|
final String status;
|
|
final String jenisTape;
|
|
|
|
HistoryModel({
|
|
required this.key,
|
|
required this.time,
|
|
required this.date,
|
|
required this.suhu,
|
|
required this.alkohol,
|
|
required this.status,
|
|
required this.jenisTape,
|
|
});
|
|
|
|
factory HistoryModel.fromMap(String key, Map data) {
|
|
// Ambil waktu sekarang sebagai cadangan (fallback)
|
|
DateTime sekarang = DateTime.now();
|
|
String jamSkrg = "${sekarang.hour.toString().padLeft(2, '0')}:${sekarang.minute.toString().padLeft(2, '0')}";
|
|
String tglSkrg = "${sekarang.day.toString().padLeft(2, '0')}/${sekarang.month.toString().padLeft(2, '0')}/${sekarang.year}";
|
|
|
|
// Logika Parsing Tanggal dari Firebase
|
|
String dateFromDb = data['tanggal']?.toString() ?? tglSkrg;
|
|
// Jika format dari DB adalah YYYY-MM-DD, ubah ke DD/MM/YYYY agar filter kalender jalan
|
|
if (dateFromDb.contains("-")) {
|
|
try {
|
|
List<String> p = dateFromDb.split("-");
|
|
if (p[0].length == 4) { // YYYY-MM-DD
|
|
dateFromDb = "${p[2]}/${p[1]}/${p[0]}";
|
|
}
|
|
} catch (e) {
|
|
dateFromDb = tglSkrg;
|
|
}
|
|
}
|
|
|
|
String timeStr = data['jam']?.toString() ?? jamSkrg;
|
|
|
|
// Ubah format desimal jam (misal "27.18 Jam") menjadi "HH:MM:SS"
|
|
if (timeStr.toLowerCase().contains("jam")) {
|
|
String numStr = timeStr.replaceAll(RegExp(r'[^0-9.]'), '');
|
|
double? val = double.tryParse(numStr);
|
|
if (val != null) {
|
|
int totalDetik = (val * 3600).toInt();
|
|
int j = totalDetik ~/ 3600;
|
|
int m = (totalDetik % 3600) ~/ 60;
|
|
int s = totalDetik % 60;
|
|
timeStr = "${j.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}";
|
|
}
|
|
}
|
|
|
|
String statusStr = data['status']?.toString() ?? "Done";
|
|
String lowerStatus = statusStr.toLowerCase();
|
|
if (lowerStatus.contains("optimal") || lowerStatus.contains("awal") || lowerStatus.contains("menjelang matang")) {
|
|
statusStr = "Belum Matang";
|
|
}
|
|
|
|
return HistoryModel(
|
|
key: key,
|
|
time: timeStr,
|
|
date: dateFromDb,
|
|
suhu: (data['suhu'] as num?)?.toDouble() ?? 0.0,
|
|
alkohol: (data['alkohol'] as num?)?.toDouble() ?? 0.0,
|
|
status: statusStr,
|
|
jenisTape: data['jenisTape'] != null
|
|
? (data['jenisTape'].toString().contains("Singkong")
|
|
? "Tape Singkong"
|
|
: "Tape Ketan")
|
|
: "Tape Ketan",
|
|
);
|
|
}
|
|
}
|
|
class HistoryPage extends StatefulWidget {
|
|
const HistoryPage({super.key});
|
|
@override
|
|
State<HistoryPage> createState() => _HistoryPageState();
|
|
}
|
|
|
|
class _HistoryPageState extends State<HistoryPage> {
|
|
final DatabaseReference ref = FirebaseDatabase.instance.ref("history");
|
|
String selectedCategory = "All";
|
|
DateTime activeDate = DateTime.now();
|
|
bool _localeReady = false;
|
|
|
|
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
initializeDateFormatting('id', null).then((_) {
|
|
if (mounted) setState(() => _localeReady = true);
|
|
});
|
|
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!_localeReady)
|
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
|
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: _buildHorizontalCalendar()),
|
|
FadeInUp(delay: 300, child: _buildCategoryFilter()),
|
|
Expanded(child: FadeInUp(delay: 450, child: _buildHistoryList())),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
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: 1,
|
|
onTap: (index) {
|
|
if (index == 0) {
|
|
Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => const DashboardPage()),
|
|
);
|
|
} 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 == 1;
|
|
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 _buildAppBar() {
|
|
return SafeArea(
|
|
bottom: false,
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 24, 20, 10), // Padding seperti di pesan.dart
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.arrow_back_ios_new, size: 20),
|
|
onPressed: () => Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => const DashboardPage()),
|
|
),
|
|
),
|
|
const Expanded(
|
|
child: Text(
|
|
"Fermentation History",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.calendar_month, color: Color(0xFF2E7D32)),
|
|
onPressed: () async {
|
|
DateTime? picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: activeDate,
|
|
firstDate: DateTime(2020),
|
|
lastDate: DateTime(2100),
|
|
);
|
|
if (picked != null && picked != activeDate) {
|
|
setState(() {
|
|
activeDate = picked;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHorizontalCalendar() {
|
|
return Container(
|
|
height: 90,
|
|
margin: const EdgeInsets.symmetric(vertical: 15), // Jarak seperti di pesan.dart
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
itemCount: 7,
|
|
itemBuilder: (context, index) {
|
|
DateTime date = activeDate.add(Duration(days: index - 3));
|
|
bool isSelected =
|
|
date.day == activeDate.day && date.month == activeDate.month;
|
|
return GestureDetector(
|
|
onTap: () => setState(() => activeDate = date),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
width: 70,
|
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? const Color(0xFF2E7D32) : Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: isSelected
|
|
? [
|
|
BoxShadow(
|
|
color: const Color(0xFF2E7D32).withOpacity(0.3),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
]
|
|
: [],
|
|
border: isSelected
|
|
? null
|
|
: Border.all(color: Colors.grey.withOpacity(0.1)),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
DateFormat('MMM', 'id').format(date),
|
|
style: TextStyle(
|
|
color: isSelected ? Colors.white : Colors.black54,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
Text(
|
|
date.day.toString(),
|
|
style: TextStyle(
|
|
color: isSelected ? Colors.white : Colors.black,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
DateFormat('E', 'id').format(date),
|
|
style: TextStyle(
|
|
color: isSelected ? Colors.white : Colors.black54,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCategoryFilter() {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Row(
|
|
children: [
|
|
_filterTab("All"),
|
|
_filterTab("Tape Ketan"),
|
|
_filterTab("Tape Singkong"),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _filterTab(String label) {
|
|
bool isSelected = selectedCategory == label;
|
|
return GestureDetector(
|
|
onTap: () => setState(() => selectedCategory = label),
|
|
child: Container(
|
|
margin: const EdgeInsets.only(right: 12),
|
|
padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? const Color(0xFF2E7D32) : const Color(0xFFE8F5E9),
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: isSelected ? Colors.white : const Color(0xFF2E7D32),
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHistoryList() {
|
|
return StreamBuilder(
|
|
stream: ref.onValue,
|
|
builder: (context, AsyncSnapshot<DatabaseEvent> snapshot) {
|
|
if (!snapshot.hasData || snapshot.data!.snapshot.value == null)
|
|
return const Center(child: Text("No history available"));
|
|
|
|
Map data = snapshot.data!.snapshot.value as Map;
|
|
List<HistoryModel> list = [];
|
|
|
|
// Format pembanding sesuai dengan format di model (dd/MM/yyyy)
|
|
String filterDateString = DateFormat('dd/MM/yyyy').format(activeDate);
|
|
|
|
data.forEach((key, value) {
|
|
final item = HistoryModel.fromMap(key, value);
|
|
|
|
// Memastikan pencocokan tanggal akurat
|
|
if (item.date == filterDateString &&
|
|
(selectedCategory == "All" || item.jenisTape == selectedCategory)) {
|
|
list.add(item);
|
|
}
|
|
});
|
|
|
|
// Urutkan dari yang terbaru (berdasarkan key Firebase atau jam)
|
|
list.sort((a, b) => b.time.compareTo(a.time));
|
|
|
|
if (list.isEmpty)
|
|
return const Center(child: Text("No data for this date"));
|
|
|
|
return ListView.builder(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
itemCount: list.length,
|
|
itemBuilder: (context, index) => _buildHistoryCard(list[index]),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildHistoryCard(HistoryModel item) {
|
|
bool isAlert =
|
|
item.status.toLowerCase().contains("matang") &&
|
|
!item.status.toLowerCase().contains("belum");
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 15),
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
border: Border.all(color: Colors.grey.withOpacity(0.05)),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
item.jenisTape,
|
|
style: const TextStyle(color: Colors.black45, fontSize: 13),
|
|
),
|
|
Text(
|
|
"Suhu: ${item.suhu}°C",
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
"Alkohol: ${item.jenisTape.contains('Singkong') ? item.alkohol.toStringAsFixed(1) : item.alkohol.toStringAsFixed(2)}%",
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
children: [
|
|
const Icon(
|
|
Icons.access_time_filled,
|
|
size: 16,
|
|
color: Color(0xFF2E7D32),
|
|
),
|
|
const SizedBox(width: 5),
|
|
Text(
|
|
item.time,
|
|
style: const TextStyle(
|
|
color: Color(0xFF2E7D32),
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(width: 15),
|
|
const Icon(
|
|
Icons.calendar_month,
|
|
size: 16,
|
|
color: Color(0xFF2E7D32),
|
|
),
|
|
const SizedBox(width: 5),
|
|
Text(
|
|
item.date,
|
|
style: const TextStyle(
|
|
color: Color(0xFF2E7D32),
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
Positioned(
|
|
right: 0,
|
|
bottom: 0,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE8F5E9),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
item.status,
|
|
style: TextStyle(
|
|
color: isAlert ? Colors.orange : const Color(0xFF2E7D32),
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|