E32221324_Iot_Running/lib/screens/home_screen.dart

426 lines
18 KiB
Dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import 'detail_hasil_screen.dart';
import 'all_activity_screen.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
Future<String> getUsername() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return '';
final doc = await FirebaseFirestore.instance.collection('users').doc(user.uid).get();
return doc.data()?['username'] ?? '';
}
Future<Map<String, double>> fetchRunningData() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return {};
final now = DateTime.now();
final startOfWeek = DateTime(now.year, now.month, now.day - (now.weekday - 1));
final endOfWeek = startOfWeek.add(const Duration(days: 7)).subtract(const Duration(seconds: 1));
final snapshot = await FirebaseFirestore.instance.collection('activities').get();
final Map<String, double> distancePerDay = {};
for (var doc in snapshot.docs) {
final data = doc.data()['data'];
if (data == null || data['userId'] != user.uid) continue;
final timestamp = (data['timestamp'] as Timestamp?)?.toDate();
if (timestamp == null || timestamp.isBefore(startOfWeek) || timestamp.isAfter(endOfWeek)) continue;
final day = DateFormat('EEEE', 'id_ID').format(timestamp);
final jarak = (data['jarak'] ?? 0).toDouble();
final satuan = data['satuan'] ?? 'M';
final jarakKm = satuan == 'M' ? jarak / 1000.0 : jarak;
distancePerDay[day] = (distancePerDay[day] ?? 0) + jarakKm;
}
const sortedDays = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'];
for (var dayName in sortedDays) {
distancePerDay.putIfAbsent(dayName, () => 0.0);
}
return distancePerDay;
}
Future<List<Map<String, dynamic>>> fetchRingkasanData() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return [];
final snapshot = await FirebaseFirestore.instance.collection('activities').get();
final List<Map<String, dynamic>> result = [];
for (var doc in snapshot.docs) {
final data = doc.data()['data']; // penting!
if (data == null || data['userId'] != user.uid) continue;
result.add({
...data,
'docId': doc.id, // tambahkan docId untuk referensi penghapusan nanti
});
}
result.sort((a, b) {
final aTime = (a['timestamp'] as Timestamp?)?.toDate() ?? DateTime(2000);
final bTime = (b['timestamp'] as Timestamp?)?.toDate() ?? DateTime(2000);
return bTime.compareTo(aTime);
});
return result;
}
String getGreeting() {
final hour = DateTime.now().hour;
if (hour >= 6 && hour <= 10) {
return 'Selamat Pagi';
} else if (hour >= 11 && hour <= 14) {
return 'Selamat Siang';
} else if (hour >= 15 && hour <= 17) {
return 'Selamat Sore';
} else {
return 'Selamat Malam';
}
}
DateTime? parseTimestamp(dynamic raw) {
if (raw is Timestamp) {
return raw.toDate();
} else if (raw is DateTime) {
return raw;
} else if (raw is String) {
return DateTime.tryParse(raw);
} else if (raw is int) {
return DateTime.fromMillisecondsSinceEpoch(raw);
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(80),
child: FutureBuilder(
future: getUsername(),
builder: (context, snapshot) {
final greeting = getGreeting();
final username = snapshot.data ?? '...';
return Container(
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
padding: const EdgeInsets.only(top: 40, left: 20, right: 20, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$greeting, $username',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Image.asset(
'images/run.png',
height: 55,
width: 55,
),
],
),
);
},
),
),
body: RefreshIndicator(
onRefresh: () async {
// Memanggil kembali data setelah pull-to-refresh
fetchRunningData();
fetchRingkasanData();
},
child: Padding(
padding: const EdgeInsets.only(top: 0.0, bottom: 0.0, right: 14.0, left: 14.0),
child: FutureBuilder(
future: Future.wait([fetchRunningData(), fetchRingkasanData(), getUsername()]),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final runningData = snapshot.data?[0] as Map<String, double>? ?? {};
final ringkasanData = snapshot.data?[1] as List<Map<String, dynamic>>? ?? [];
final sortedDays = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'];
final values = sortedDays.map((day) => runningData[day] ?? 0.0).toList();
final totalMinggu = values.fold(0.0, (a, b) => a + b);
int totalDurasiDetik = 0;
final now = DateTime.now();
final startOfWeek = DateTime(now.year, now.month, now.day - (now.weekday - 1));
final endOfWeek = startOfWeek.add(const Duration(days: 7)).subtract(const Duration(seconds: 1));
for (var data in ringkasanData) {
final timestamp = parseTimestamp(data['timestamp']);
if (timestamp == null) continue;
if (timestamp.isBefore(startOfWeek) || timestamp.isAfter(endOfWeek)) continue;
totalDurasiDetik += (((data['duration'] ?? data['durasi'] ?? 0) as num).toInt() ~/ 1000);
}
String formatDurasi(int totalSeconds) {
final jam = totalSeconds ~/ 3600;
final menit = (totalSeconds % 3600) ~/ 60;
final detik = totalSeconds % 60;
return '${jam}j ${menit}m ${detik}d';
}
final totalDurasiFormatted = formatDurasi(totalDurasiDetik);
final maxValue = values.reduce((a, b) => a > b ? a : b);
final highestDay = sortedDays[values.indexOf(maxValue)];
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Minggu ini',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
),
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AllActivityScreen()),
);
},
child: const Text(
'View all weeks',
style: TextStyle(color: Colors.blue),
),
),
],
),
const SizedBox(height: 1),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Jarak',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
),
Text(
'${totalMinggu.toStringAsFixed(2)} km',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(width: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Waktu',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
),
Text(
totalDurasiFormatted,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
],
),
const SizedBox(height: 20),
SizedBox(
height: 170,
child: Padding(
padding: const EdgeInsets.only(left: 5.0, right: 6.0),
child: LineChart(
LineChartData(
minY: 0.0,
maxY: (values.isEmpty ? 5.0 : values.reduce((a, b) => a > b ? a : b)),
gridData: FlGridData(
show: true,
drawVerticalLine: true,
drawHorizontalLine: false,
verticalInterval: 1,
getDrawingVerticalLine: (value) {
return FlLine(
color: Colors.blue.withOpacity(0.2),
strokeWidth: 1,
);
},
),
extraLinesData: ExtraLinesData(
verticalLines: [
VerticalLine(
x: 0,
color: Colors.blue.withOpacity(0.2),
strokeWidth: 1,
),
VerticalLine(
x: (sortedDays.length - 1).toDouble(),
color: Colors.blue.withOpacity(0.2),
strokeWidth: 1,
),
],
),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: (value, meta) {
int index = value.toInt();
if (index < 0 || index >= sortedDays.length) {
return const SizedBox.shrink();
}
EdgeInsets padding = EdgeInsets.zero;
if (index == 0) {
padding = const EdgeInsets.only(left: 12);
} else if (index == sortedDays.length - 1) {
padding = const EdgeInsets.only(right: 12);
}
return Padding(
padding: padding.add(const EdgeInsets.only(top: 4)), // tambah jarak dari grafik ke teks
child: Text(
sortedDays[index].substring(0, 3),
style: const TextStyle(fontSize: 10),
),
);
},
),
),
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 35,
getTitlesWidget: (value, _) {
if (value == 0.0 || value == maxValue || value == (maxValue + 1)) {
return Padding(
padding: const EdgeInsets.only(left: 8),
child: Text('${value.toStringAsFixed(1)} KM', style: const TextStyle(fontSize: 10)),
);
}
return const SizedBox.shrink();
},
),
),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: List.generate(values.length, (i) => FlSpot(i.toDouble(), values[i])),
isCurved: false,
barWidth: 3,
dotData: FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: Colors.lightBlueAccent.withOpacity(0.3),
),
color: Colors.blue,
),
],
),
),
),
),
const SizedBox(height: 24),
Text('Hari Teraktif: $highestDay (${maxValue.toStringAsFixed(2)} KM)',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 16),
const Text('Ringkasan Hasil Lari',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
ringkasanData.isEmpty
? const Text('Belum ada data untuk hari teraktif.')
: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: ringkasanData.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final data = ringkasanData[index];
final timestamp = (data['timestamp'] as Timestamp?)?.toDate();
if (timestamp == null) return const SizedBox();
final jarak = (data['jarak'] ?? 0).toDouble();
final satuan = data['satuan'] ?? 'M';
final jarakKm = satuan == 'M' ? jarak / 1000.0 : jarak;
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailHasilLariScreen(
data: {
...data,
'docId': ringkasanData[index]['docId'] ?? '', // tambahkan docId kalau tersedia
},
),
),
);
},
child: Card(
color: const Color.fromARGB(255, 60, 193, 255),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Padding(
padding: const EdgeInsets.only(top: 12.0), // geser ke bawah
child: const Icon(Icons.directions_run),
),
title: Text('${jarak.toStringAsFixed(0)} $satuan'),
subtitle: Text(
'${DateFormat('dd MMM yyyy, HH:mm').format(timestamp)}\n(${jarakKm.toStringAsFixed(2)} KM)',
),
isThreeLine: true,
trailing: Padding(
padding: const EdgeInsets.only(top: 12.0), // geser ke bawah
child: const Icon(Icons.arrow_forward_ios, size: 16),
),
),
),
);
},
),
],
),
);
},
),
),
)
);
}
}