430 lines
16 KiB
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)),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|