E32221324_Iot_Running/lib/screens/event_result_page.dart

379 lines
15 KiB
Dart

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.dart';
class EventResultPage extends StatelessWidget {
final String eventId;
final String eventName;
EventResultPage({required this.eventId, required this.eventName});
final Color primaryBlue = Color(0xFF42A5F5);
final Color lightBlue = Color(0xFFBBDEFB);
final Color darkBlue = Color(0xFF1976D2);
final Color successGreen = Colors.green.shade700;
final Color warningOrange = Colors.orange.shade700;
final Color dangerRed = Colors.red.shade700;
@override
Widget build(BuildContext context) {
final docRef = FirebaseFirestore.instance.collection('events').doc(eventId);
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
title: Text(
'Hasil Event: $eventName',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: primaryBlue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
iconTheme: IconThemeData(color: Colors.white),
elevation: 5,
),
body: StreamBuilder<DocumentSnapshot>(
stream: docRef.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator(color: primaryBlue));
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}', style: TextStyle(color: dangerRed)));
}
if (!snapshot.hasData || !snapshot.data!.exists) {
return Center(child: Text('Event tidak ditemukan.', style: TextStyle(color: Colors.grey[600])));
}
final data = snapshot.data!.data() as Map<String, dynamic>?;
if (data == null || data['participants'] == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.people_alt_outlined, size: 60, color: Colors.grey[400]),
SizedBox(height: 16),
Text('Belum ada peserta yang terdaftar untuk event ini.', style: TextStyle(fontSize: 16, color: Colors.grey[600])),
],
),
);
}
final participants = data['participants'] as Map<String, dynamic>;
if (participants.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.people_alt_outlined, size: 60, color: Colors.grey[400]),
SizedBox(height: 16),
Text('Belum ada peserta yang terdaftar untuk event ini.', style: TextStyle(fontSize: 16, color: Colors.grey[600])),
],
),
);
}
List<MapEntry<String, dynamic>> sortedParticipants = participants.entries.toList();
sortedParticipants.sort((a, b) {
final pA = a.value as Map<String, dynamic>;
final pB = b.value as Map<String, dynamic>;
final finishedA = pA['finished'] == true;
final finishedB = pB['finished'] == true;
if (finishedA && !finishedB) return -1;
if (!finishedA && finishedB) return 1;
if (finishedA && finishedB) {
final timeA = _parseTimeToSeconds(pA['total_time']);
final timeB = _parseTimeToSeconds(pB['total_time']);
return timeA.compareTo(timeB);
}
final lapsA = pA['laps'] ?? 0;
final lapsB = pB['laps'] ?? 0;
return lapsB.compareTo(lapsA);
});
return ListView.builder(
padding: EdgeInsets.all(16),
itemCount: sortedParticipants.length,
itemBuilder: (context, index) {
final entry = sortedParticipants[index];
final p = entry.value as Map<String, dynamic>;
final name = p['name'] ?? 'Peserta Tidak Dikenal';
final laps = p['laps'] ?? 0;
final finished = p['finished'] == true;
final totalTime = p['total_time'] ?? '-';
final rfidUid = p['rfid_uid'] ?? '-';
final lapDurations = p['lap_durations'] as Map<String, dynamic>? ?? {};
String participantStatus = finished ? 'Selesai' : 'Berlangsung';
Color participantStatusColor = finished ? successGreen : warningOrange;
String formattedTotalTime = '-';
if (finished && totalTime != '-') {
try {
final duration = Duration(milliseconds: _parseMilliseconds(totalTime));
formattedTotalTime = _formatDuration(duration);
} catch (e) {
formattedTotalTime = totalTime;
}
}
List<Widget> kecepatanWidgets = [];
final sortedLapKeys = lapDurations.keys.toList()..sort((a, b) {
final intA = int.parse(a.split('_')[1]);
final intB = int.parse(b.split('_')[1]);
return intA.compareTo(intB);
});
for (String lapKey in sortedLapKeys) {
final dynamic durasiDetikRaw = lapDurations[lapKey];
double durasiDetik = 0.0;
if (durasiDetikRaw is int) {
durasiDetik = durasiDetikRaw.toDouble();
} else if (durasiDetikRaw is double) {
durasiDetik = durasiDetikRaw;
}
String formattedDuration = '${durasiDetik.toStringAsFixed(2)} s';
if (durasiDetik > 0) {
final kecepatan = 400 / durasiDetik;
kecepatanWidgets.add(
Row(
children: [
SizedBox(width: 24),
Text(
"Lap ${lapKey.split('_')[1]}: ${kecepatan.toStringAsFixed(2)} m/s ",
style: TextStyle(fontSize: 13, color: Colors.grey[800]),
),
Text(
"($formattedDuration)",
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
);
} else {
kecepatanWidgets.add(
Padding(
padding: const EdgeInsets.only(left: 24.0),
child: Text(
"Lap ${lapKey.split('_')[1]}: Durasi tidak valid",
style: TextStyle(fontSize: 13, color: Colors.grey[500], fontStyle: FontStyle.italic),
),
),
);
}
}
return Card(
margin: EdgeInsets.only(bottom: 16),
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: darkBlue,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: participantStatusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
participantStatus,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: participantStatusColor,
),
),
),
],
),
SizedBox(height: 8),
Divider(color: Colors.grey[200]),
SizedBox(height: 8),
_buildInfoRow("RFID Tag", rfidUid, darkBlue),
_buildInfoRow("Putaran Selesai", "$laps", darkBlue),
if (finished) _buildInfoRow("Waktu Total", formattedTotalTime, darkBlue),
if (kecepatanWidgets.isNotEmpty) ...[
SizedBox(height: 12),
Text(
"Detail Kecepatan per Lap:",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: darkBlue,
),
),
SizedBox(height: 8),
...kecepatanWidgets,
],
// Tombol hapus peserta
SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: IconButton( // <-- DIUBAH DARI TextButton.icon MENJADI IconButton
icon: Icon(Icons.delete_outline, color: dangerRed, size: 24), // Ukuran ikon diperbesar sedikit
tooltip: 'Hapus Peserta', // Tooltip tetap ada saat ditekan lama
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('Konfirmasi Hapus Peserta', style: TextStyle(color: dangerRed)),
content: Text('Apakah Anda yakin ingin menghapus peserta "$name" dari event ini?', style: TextStyle(color: Colors.grey[800])),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Batal', style: TextStyle(color: primaryBlue)),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Hapus', style: TextStyle(color: dangerRed)),
),
],
),
);
if (confirm == true) {
try {
await docRef.update({
'participants.${entry.key}': FieldValue.delete(),
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Peserta "$name" berhasil dihapus.'), backgroundColor: Colors.green),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal menghapus peserta: $e'), backgroundColor: Colors.red),
);
print('Error deleting participant: $e');
}
}
},
),
),
],
),
),
);
},
);
},
),
);
}
// Helper widget untuk baris informasi yang clean tanpa ikon
Widget _buildInfoRow(String label, String value, Color valueColor) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Text(
"$label:",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14, color: Colors.grey[700]),
),
),
Expanded(
flex: 3,
child: Text(
value,
style: TextStyle(fontSize: 15, color: valueColor),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
// Helper untuk parsing waktu (misal "00:01:23.456" menjadi milidetik)
int _parseMilliseconds(String timeString) {
if (timeString == '-') return 0;
final parts = timeString.split(':');
if (parts.length < 3) return 0; // Invalid format
int hours = int.parse(parts[0]);
int minutes = int.parse(parts[1]);
double secondsWithMs = double.parse(parts[2]);
int totalMilliseconds = ((hours * 3600 + minutes * 60 + secondsWithMs) * 1000).toInt();
return totalMilliseconds;
}
// Helper untuk format total_time dari milidetik
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, "0");
String threeDigits(int n) => n.toString().padLeft(3, "0");
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
String milliseconds = threeDigits(duration.inMilliseconds.remainder(1000));
String hours = twoDigits(duration.inHours);
if (duration.inHours > 0) {
return "$hours:$minutes:$seconds.$milliseconds";
} else {
return "$minutes:$seconds.$milliseconds";
}
}
// Helper untuk mengonversi string waktu (HH:MM:SS.ms) ke total detik (untuk sorting)
double _parseTimeToSeconds(String timeString) {
if (timeString == '-') return double.infinity;
try {
final parts = timeString.split(':');
if (parts.length != 3) return double.infinity;
final hours = int.parse(parts[0]);
final minutes = int.parse(parts[1]);
final secondsParts = parts[2].split('.');
final seconds = int.parse(secondsParts[0]);
final milliseconds = secondsParts.length > 1 ? int.parse(secondsParts[1]) : 0;
return (hours * 3600 + minutes * 60 + seconds + milliseconds / 1000).toDouble();
} catch (e) {
return double.infinity;
}
}
}