E32221324_Iot_Running/lib/screens/detail_hasil_screen.dart

430 lines
16 KiB
Dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class DetailHasilLariScreen extends StatefulWidget {
final Map<String, dynamic> data;
const DetailHasilLariScreen({super.key, required this.data});
@override
State<DetailHasilLariScreen> createState() => _DetailHasilLariScreenState();
}
class _DetailHasilLariScreenState extends State<DetailHasilLariScreen> {
List<double> kecepatanList = [];
bool loading = true;
@override
void initState() {
super.initState();
fetchLapData();
}
Future<void> fetchLapData() async {
final jenis = widget.data['type'];
// =====================
// NON-LINTASAN
// =====================
if (jenis == 'non-lintasan') {
final duration = widget.data['duration'];
final rawJarak = widget.data['jarak'] ?? widget.data['distance'] ?? 0.0;
final satuan = widget.data['satuan'] ?? 'M';
final jarakKm = satuan == 'KM' ? rawJarak.toDouble() : rawJarak.toDouble() / 1000.0;
if (duration is int && jarakKm > 0) {
final durSec = duration / 1000;
final pacePerKmSec = durSec / jarakKm;
// Gunakan floor agar hanya simulasikan per-km (jangan 1.5 jadi 2)
List<double> tempPaces = List.generate(jarakKm.floor(), (_) => pacePerKmSec);
widget.data['pace_sec_per_km'] = pacePerKmSec;
setState(() {
kecepatanList = tempPaces;
loading = false;
});
return;
}
}
// =====================
// LINTASAN
// =====================
final docId = widget.data['docId'];
final totalLaps = widget.data['putaran'];
if (docId == null || totalLaps == null) {
setState(() => loading = false);
return;
}
final doc = await FirebaseFirestore.instance.collection('activities').doc(docId).get();
final laps = doc['data']['laps'];
if (laps == null || laps is! Map) {
setState(() => loading = false);
return;
}
final rawStart = widget.data['startTime'];
DateTime? startTime;
if (rawStart is Timestamp) {
startTime = rawStart.toDate();
} else if (rawStart is int) {
startTime = DateTime.fromMillisecondsSinceEpoch(rawStart);
}
final lapZero = laps['lap_0'];
DateTime? prevTime;
if (lapZero != null && lapZero['timestamp'] != null) {
prevTime = DateTime.parse(lapZero['timestamp']);
} else if (startTime != null) {
prevTime = startTime;
}
List<double> tempSpeeds = [];
final jarak = widget.data['jarak'] ?? 0;
final jarakPerLap = jarak / totalLaps;
for (int i = 1; i <= totalLaps; i++) {
final lapData = laps['lap_$i'];
if (lapData == null || lapData['timestamp'] == null) continue;
final timestamp = DateTime.parse(lapData['timestamp']);
if (prevTime != null) {
final durationMs = timestamp.difference(prevTime).inMilliseconds;
if (durationMs <= 0) {
tempSpeeds.add(0.0);
} else {
final speed = jarakPerLap / (durationMs / 1000);
tempSpeeds.add(speed);
}
}
prevTime = timestamp;
}
setState(() {
kecepatanList = tempSpeeds;
loading = false;
});
}
void _confirmDelete(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Hapus Aktivitas?'),
content: const Text('Apakah kamu yakin ingin menghapus aktivitas ini?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Batal'),
),
TextButton(
onPressed: () async {
Navigator.pop(ctx);
await _deleteActivity(context);
},
child: const Text('Hapus', style: TextStyle(color: Colors.red)),
),
],
),
);
}
Future<void> _deleteActivity(BuildContext context) async {
try {
final docId = widget.data['docId'];
if (docId != null) {
await FirebaseFirestore.instance.collection('activities').doc(docId).delete();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Aktivitas berhasil dihapus')),
);
Navigator.pop(context);
} else {
throw Exception('ID dokumen tidak ditemukan');
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal menghapus: $e')),
);
}
}
String getRunPeriodLabel(DateTime time) {
final hour = time.hour;
if (hour >= 6 && hour <= 10) {
return 'Berlari Pagi';
} else if (hour >= 11 && hour <= 14) {
return 'Berlari Siang';
} else if (hour >= 15 && hour <= 17) {
return 'Berlari Sore';
} else {
return 'Berlari Malam';
}
}
@override
Widget build(BuildContext context) {
final data = widget.data;
final timestamp = (data['timestamp'] as Timestamp).toDate();
final jenis = data['type'] == 'lintasan' ? 'Lintasan' : 'Non-Lintasan';
final duration = data['duration'];
DateTime? startTime;
final rawStart = data['startTime'];
if (rawStart is Timestamp) {
startTime = rawStart.toDate();
} else if (rawStart is int) {
startTime = DateTime.fromMillisecondsSinceEpoch(rawStart);
}
final formattedStart = startTime != null ? DateFormat('HH:mm:ss').format(startTime) : '-';
String durationFormatted = '-';
if (duration is int) {
final d = Duration(milliseconds: duration);
durationFormatted = d.toString().split('.').first.padLeft(8, "0");
}
String? paceFormatted;
if (data['type'] == 'non-lintasan' && data['pace_sec_per_km'] != null) {
final paceSec = (data['pace_sec_per_km'] as num).toDouble();
final paceMin = paceSec ~/ 60;
final paceSecRem = (paceSec % 60).round();
paceFormatted = '$paceMin:${paceSecRem.toString().padLeft(2, '0')} /km';
}
final rawJarak = data['jarak'] ?? data['distance'] ?? 0.0;
final satuan = data['satuan'] ?? 'M';
final jarakKm = satuan == 'KM' ? rawJarak.toDouble() : rawJarak.toDouble() / 1000.0;
final putaran = data['putaran'];
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(100),
child: 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: 13),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
const Text(
"Detail Hasil Lari",
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.white),
tooltip: 'Hapus Aktivitas',
onPressed: () => _confirmDelete(context),
),
],
),
),
),
backgroundColor: Colors.white,
body: Padding(
padding: const EdgeInsets.only(top: 30, left: 20, right: 20, bottom: 16),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade300, Colors.blue.shade600],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
offset: const Offset(0, 4),
blurRadius: 8,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Text(
getRunPeriodLabel(timestamp),
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(height: 4),
Text(
DateFormat('dd MMMM yyyy, HH:mm').format(timestamp),
style: const TextStyle(color: Colors.white70, fontSize: 14),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// KIRI
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
durationFormatted,
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text('Durasi', style: TextStyle(color: Colors.white70)),
if (data['type'] == 'non-lintasan' && paceFormatted != null) ...[
const SizedBox(height: 12),
Text(
paceFormatted!,
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text('Pace', style: TextStyle(color: Colors.white70)),
],
if (data['type'] == 'lintasan') ...[
const SizedBox(height: 12),
Text(
'$putaran',
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text('Putaran', style: TextStyle(color: Colors.white70)),
],
],
),
// KANAN
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${jarakKm.toStringAsFixed(2)} KM',
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text('Jarak', style: TextStyle(color: Colors.white70)),
const SizedBox(height: 12),
Text(
jenis,
style: const TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text('Jenis Aktivitas', style: TextStyle(color: Colors.white70)),
],
),
],
),
const SizedBox(height: 16),
],
),
),
const SizedBox(height: 32),
if (data['type'] == 'lintasan') ...[
const SizedBox(height: 32),
const Text("Splits Kecepatan", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
loading
? const Center(child: CircularProgressIndicator())
: kecepatanList.isEmpty
? const Text("Data kecepatan tidak tersedia.")
: Column(
children: List.generate(kecepatanList.length, (index) {
final speed = kecepatanList[index];
final maxSpeed = kecepatanList.reduce((a, b) => a > b ? a : b);
final barFraction = (speed / maxSpeed).clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
children: [
SizedBox(width: 40, child: Text("Lap ${index + 1}", style: const TextStyle(fontSize: 14))),
const SizedBox(width: 8),
Expanded(
child: Stack(
children: [
Container(
height: 20,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6),
),
),
FractionallySizedBox(
widthFactor: barFraction,
child: Container(
height: 20,
decoration: BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.circular(6),
),
),
),
],
),
),
const SizedBox(width: 8),
Text("${speed.toStringAsFixed(2)} m/s", style: const TextStyle(fontSize: 14)),
],
),
);
}),
),
],
],
),
),
),
);
}
}