E32221324_Iot_Running/lib/screens/event_detail_page.dart

816 lines
38 KiB
Dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'dart:async';
class EventDetailPage extends StatefulWidget {
final String eventId;
final String eventName;
EventDetailPage({required this.eventId, required this.eventName});
@override
_EventDetailPageState createState() => _EventDetailPageState();
}
class _EventDetailPageState extends State<EventDetailPage> {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
late FirebaseDatabase _rtdb;
late DatabaseReference _eventStatusRef;
late DatabaseReference _scanResultRef;
late DatabaseReference _eventActivityDataRef;
// Map untuk menyimpan status finished dan current laps DARI RTDB (Hanya relevan saat event aktif)
Map<String, bool> _liveParticipantFinishedStatus = {};
Map<String, int> _liveParticipantCurrentLaps = {};
// Map untuk mapping UID RFID ke Firestore userId
Map<String, String> _rfidUidToUserIdMap = {};
// List untuk menyimpan subscriptions agar bisa di-cancel saat dispose
final List<StreamSubscription> _rtdbSubscriptions = [];
// Variabel untuk menyimpan status event global dari RTDB (hanya untuk kontrol internal sementara)
bool _rtdbEventStarted = false;
bool _rtdbEventFinished = false;
// StreamSubscription untuk Realtime Database listeners
StreamSubscription? _activityDataSubscription;
StreamSubscription? _finishedSubscription;
StreamSubscription? _lapsSubscription;
StreamSubscription? _scanResultSubscription;
// Warna utama yang terinspirasi dari gambar Anda
final Color primaryBlue = Color(0xFF42A5F5); // Biru muda yang cerah
final Color lightBlue = Color(0xFFBBDEFB); // Biru yang lebih terang untuk background
final Color darkBlue = Color(0xFF1976D2); // Biru yang lebih gelap untuk teks/ikon
@override
void initState() {
super.initState();
_rtdb = FirebaseDatabase.instanceFor(
app: Firebase.app(),
databaseURL: "https://ta-running-default-rtdb.asia-southeast1.firebasedatabase.app",
);
_eventStatusRef = _rtdb.ref('event_status/${widget.eventId}');
_scanResultRef = _rtdb.ref('scan_result/${widget.eventId}');
_eventActivityDataRef = _rtdb.ref('event_activity_data/${widget.eventId}');
_preloadRfidToUserIdsMap().then((_) {
_listenToRealtimeEventStatus(); // Mendengarkan status global dari RTDB (untuk internal saja)
});
}
@override
void dispose() {
for (var subscription in _rtdbSubscriptions) {
subscription.cancel();
}
super.dispose();
}
Future<void> _preloadRfidToUserIdsMap() async {
try {
final eventDoc = await _firestore.collection('events').doc(widget.eventId).get();
if (eventDoc.exists) {
final participantsData = eventDoc.data()?['participants'] as Map<String, dynamic>?;
if (participantsData != null) {
participantsData.forEach((userId, data) {
final rfidUid = data['rfid_uid'] as String?;
if (rfidUid != null) {
_rfidUidToUserIdMap[rfidUid] = userId;
}
});
print("Preloaded RFID UID to User ID Map: $_rfidUidToUserIdMap");
}
}
} catch (e) {
print("Error preloading RFID UID to User ID map: $e");
}
}
// Listener Realtime Database untuk status event global (hanya untuk tujuan internal)
void _listenToRealtimeEventStatus() {
_rtdbSubscriptions.add(_eventStatusRef.onValue.listen((event) async {
if (event.snapshot.exists && event.snapshot.value != null) {
final eventStatusData = event.snapshot.value as Map<dynamic, dynamic>;
setState(() {
_rtdbEventStarted = eventStatusData['started'] ?? false;
_rtdbEventFinished = eventStatusData['finished'] ?? false;
});
// Ambil status permanen dari Firestore untuk perbandingan
final firestoreDoc = await _firestore.collection('events').doc(widget.eventId).get();
final firestoreData = firestoreDoc.data();
bool isEventFinishedPermanently = firestoreData?['is_finished_permanently'] ?? false;
// Jika event baru saja selesai di RTDB oleh ESP32, dan belum di Firestore
if (_rtdbEventFinished && !isEventFinishedPermanently) {
_endEventPermanentlyFromRTDB();
}
} else {
setState(() {
_rtdbEventStarted = false;
_rtdbEventFinished = false;
});
}
print("Internal RTDB Event Status: Started=$_rtdbEventStarted, Finished=$_rtdbEventFinished");
}));
}
// Fungsi baru: jika RTDB menandakan selesai, dan Firestore belum, akhiri permanen
Future<void> _endEventPermanentlyFromRTDB() async {
print("RTDB menandakan event selesai, mengakhiri secara permanen di Firestore.");
try {
await _firestore.collection('events').doc(widget.eventId).update({
'is_started_permanently': false,
'is_finished_permanently': true,
});
// Bersihkan RTDB setelah status permanen di Firestore diupdate
await _rtdb.ref('event_activity_data/${widget.eventId}').remove();
await _rtdb.ref('event_status/${widget.eventId}').remove();
await _rtdb.ref('scan_result/${widget.eventId}').remove();
print("✅ Event diakhiri secara permanen di Firestore dan RTDB dibersihkan.");
} catch (e) {
print("❌ Gagal mengakhiri event secara permanen dari RTDB: $e");
}
}
// Fungsi untuk mengelola listener Realtime Database
void _manageRealtimeListeners(bool activate) {
if (activate) {
// Aktifkan listener aktivitas peserta jika belum ada
if (_activityDataSubscription == null) {
print("Mengaktifkan Realtime Database activity listeners...");
// Gunakan onChildAdded untuk inisialisasi awal dan onChildChanged untuk update
_activityDataSubscription = _eventActivityDataRef.onChildAdded.listen((event) {
final uidTag = event.snapshot.key;
if (uidTag != null) {
// Subscribe ke 'finished'
_finishedSubscription = _eventActivityDataRef.child(uidTag).child('finished').onValue.listen((finishedEvent) async {
if (finishedEvent.snapshot.exists && finishedEvent.snapshot.value != null) {
final finished = finishedEvent.snapshot.value as bool;
setState(() {
_liveParticipantFinishedStatus[uidTag] = finished;
});
final userId = _rfidUidToUserIdMap[uidTag];
if (userId != null) {
try {
await _firestore.collection('events').doc(widget.eventId).update({
'participants.$userId.finished': finished,
});
if (finished) {
final totalTimeSnapshot = await _eventActivityDataRef.child(uidTag).child('duration').get();
if (totalTimeSnapshot.exists && totalTimeSnapshot.value != null) {
final totalTimeMs = totalTimeSnapshot.value as int;
String formattedTime = _formatDuration(Duration(milliseconds: totalTimeMs));
await _firestore.collection('events').doc(widget.eventId).update({
'participants.$userId.total_time': formattedTime,
});
}
}
} catch (e) {
print("❌ Gagal update Firestore participants.$userId.finished/total_time: $e");
}
}
}
});
_rtdbSubscriptions.add(_finishedSubscription!);
// Subscribe ke 'laps'
_lapsSubscription = _eventActivityDataRef.child(uidTag).child('laps').onValue.listen((lapsEvent) async {
if (lapsEvent.snapshot.exists && lapsEvent.snapshot.value != null) {
final lapsData = lapsEvent.snapshot.value as Map<dynamic, dynamic>;
final lapsCount = lapsData.length;
setState(() {
_liveParticipantCurrentLaps[uidTag] = lapsCount;
});
final userId = _rfidUidToUserIdMap[uidTag];
if (userId != null) {
try {
await _firestore.collection('events').doc(widget.eventId).update({
'participants.$userId.laps': lapsCount > 0 ? lapsCount - 1 : 0,
});
Map<String, DateTime> currentTimestamps = {};
lapsData.forEach((key, value) {
if (value is Map && value.containsKey('timestamp')) {
currentTimestamps[key.toString()] = DateTime.parse(value['timestamp']);
}
});
Map<String, double> lapDurations = {};
final sortedKeys = currentTimestamps.keys.toList()..sort((a, b) => a.compareTo(b));
for (int i = 1; i < sortedKeys.length; i++) {
final prevTimestamp = currentTimestamps[sortedKeys[i - 1]];
final currentTimestamp = currentTimestamps[sortedKeys[i]];
if (prevTimestamp != null && currentTimestamp != null) {
final durationSeconds = currentTimestamp.difference(prevTimestamp).inMilliseconds / 1000.0;
lapDurations['lap_${i}'] = durationSeconds;
}
}
if (lapDurations.isNotEmpty) {
await _firestore.collection('events').doc(widget.eventId).update({
'participants.$userId.lap_durations': lapDurations,
});
}
} catch (e) {
print("❌ Gagal update Firestore participants.$userId.laps atau lap_durations: $e");
}
}
}
});
_rtdbSubscriptions.add(_lapsSubscription!);
}
});
_rtdbSubscriptions.add(_activityDataSubscription!);
}
// Aktifkan listener scan RFID jika belum aktif
if (_scanResultSubscription == null) {
print("Mengaktifkan scan_result listener...");
final scanRef = _rtdb.ref('scan_result/${widget.eventId}');
_scanResultSubscription = scanRef.onChildAdded.listen((event) {
final scannedUID = event.snapshot.key;
final value = event.snapshot.value;
if (scannedUID != null && value == true) {
processUID(scannedUID, widget.eventId);
}
});
_rtdbSubscriptions.add(_scanResultSubscription!);
// Panggil processUID untuk data yang sudah ada saat ini juga (saat listener diaktifkan)
scanRef.get().then((snapshot) {
if (snapshot.exists) {
final data = snapshot.value as Map<dynamic, dynamic>;
data.forEach((key, value) {
if (value == true) {
processUID(key, widget.eventId);
}
});
}
});
}
} else {
// Nonaktifkan semua listener jika event tidak dimulai secara permanen atau sudah selesai
print("Menonaktifkan Realtime Database listeners...");
_activityDataSubscription?.cancel();
_finishedSubscription?.cancel();
_lapsSubscription?.cancel();
_scanResultSubscription?.cancel();
// Set semua subscription ke null agar bisa diinisialisasi ulang
_activityDataSubscription = null;
_finishedSubscription = null;
_lapsSubscription = null;
_scanResultSubscription = null;
// Juga kosongkan data live RTDB jika listener dimatikan
_liveParticipantFinishedStatus.clear();
_liveParticipantCurrentLaps.clear();
}
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
String threeDigitMilliseconds = (duration.inMilliseconds.remainder(1000)).toString().padLeft(3, "0");
return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds.$threeDigitMilliseconds";
}
void processUID(String scannedUID, String eventId) async {
print("🔍 Mencari user dengan idGelang = '$scannedUID'");
final querySnapshot = await _firestore
.collection('users')
.where('idGelang', isEqualTo: scannedUID.trim())
.limit(1)
.get();
if (querySnapshot.docs.isEmpty) {
print('❌ UID $scannedUID tidak ditemukan di Firestore.');
return;
}
final userDoc = querySnapshot.docs.first;
final userId = userDoc.id;
final userData = userDoc.data();
_rfidUidToUserIdMap[scannedUID] = userId;
print("Mapped RFID UID $scannedUID to User ID $userId");
final eventRef = _firestore.collection('events').doc(eventId);
final eventSnapshot = await eventRef.get();
Map<String, dynamic> participants = eventSnapshot.data()?['participants'] ?? {};
Map<String, dynamic> existing = participants[userId] ?? {};
final participantData = {
'name': userData['username'] ?? existing['name'] ?? '-',
'user_id': userId,
'rfid_uid': scannedUID,
'laps': existing['laps'] ?? 0,
'finished': existing['finished'] ?? false,
'total_time': existing['total_time'] ?? '-',
'lap_durations': existing['lap_durations'] ?? {},
};
await eventRef.update({
'participants.$userId': participantData,
});
print("🎉 Peserta ${participantData['name']} berhasil dimasukkan/diupdate di event $eventId di Firestore.");
}
@override
Widget build(BuildContext context) {
final docRef = _firestore.collection('events').doc(widget.eventId);
return Scaffold(
backgroundColor: Colors.grey[50], // Background lembut
body: StreamBuilder<DocumentSnapshot>(
stream: docRef.snapshots(),
builder: (context, firestoreSnapshot) {
if (!firestoreSnapshot.hasData) {
return Center(child: CircularProgressIndicator(color: primaryBlue));
}
if (firestoreSnapshot.hasError) {
return Center(child: Text('Error: ${firestoreSnapshot.error}', style: TextStyle(color: Colors.red)));
}
final firestoreData = firestoreSnapshot.data!.data() as Map<String, dynamic>?;
if (firestoreData == null) {
return Center(child: Text('Data event tidak ditemukan di Firestore.'));
}
// Ambil status permanen dari Firestore
bool isEventStartedPermanently = firestoreData['is_started_permanently'] ?? false;
bool isEventFinishedPermanently = firestoreData['is_finished_permanently'] ?? false;
// Kelola listener RTDB berdasarkan status permanen event
WidgetsBinding.instance.addPostFrameCallback((_) {
_manageRealtimeListeners(isEventStartedPermanently && !isEventFinishedPermanently);
});
// Logic untuk menentukan status yang ditampilkan di UI
String displayEventStatusText;
Color displayEventStatusColor;
if (isEventFinishedPermanently) {
displayEventStatusText = "Sudah Selesai Permanen";
displayEventStatusColor = Colors.green.shade700;
} else if (isEventStartedPermanently) {
displayEventStatusText = "Sedang Berlangsung";
displayEventStatusColor = Colors.orange.shade700;
} else {
displayEventStatusText = "Belum Dimulai";
displayEventStatusColor = Colors.red.shade700;
}
final totalLaps = firestoreData['total_laps'] ?? 0;
final totalDistance = firestoreData['total_distance'] ?? 0;
final createdBy = firestoreData['created_by'] ?? '-';
final createdAt = firestoreData['created_at'] != null
? (firestoreData['created_at'] as Timestamp).toDate()
: null;
return CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 180.0, // Tinggi AppBar saat expanded
floating: false,
pinned: true, // AppBar tetap terlihat saat scroll
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20), // Sudut melengkung di kiri bawah
bottomRight: Radius.circular(20), // Sudut melengkung di kanan bawah
),
),
backgroundColor: primaryBlue, // Warna dasar AppBar
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
titlePadding: EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0),
// Pastikan background juga melengkung
background: ClipRRect( // Tambahkan ClipRRect di sini
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [primaryBlue, darkBlue],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.only(top: 24.0), // Beri sedikit padding atas agar tidak terlalu mepet
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.event,
color: Colors.white,
size: 48,
),
SizedBox(height: 8),
Text(
widget.eventName,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
shadows: [
Shadow(
blurRadius: 4.0,
color: Colors.black.withOpacity(0.3),
offset: Offset(1.0, 1.0),
),
],
),
textAlign: TextAlign.center,
),
SizedBox(height: 4),
Text(
"${totalLaps} Putaran | ${(totalDistance / 1000).toStringAsFixed(2)} KM",
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
),
),
),
),
),
actions: [
IconButton(
icon: Icon(Icons.delete_forever, color: Colors.white),
tooltip: 'Hapus Event',
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('Konfirmasi Hapus', style: TextStyle(color: darkBlue)),
content: Text('Apakah Anda yakin ingin menghapus event "${widget.eventName}" dan semua datanya?', 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: Colors.red)),
),
],
),
);
if (confirm == true) {
await docRef.delete();
await _rtdb.ref('event_activity_data/${widget.eventId}').remove();
await _rtdb.ref('event_status/${widget.eventId}').remove();
await _rtdb.ref('scan_result/${widget.eventId}').remove();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Event "${widget.eventName}" telah dihapus.')),
);
}
},
),
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Informasi Detail Event
Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
color: Colors.white, // CARD PUTIH
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(Icons.person, "Dibuat oleh", createdBy),
SizedBox(height: 8),
if (createdAt != null)
_buildInfoRow(
Icons.calendar_today,
"Tanggal dibuat",
"${createdAt.day}/${createdAt.month}/${createdAt.year} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}",
),
SizedBox(height: 16),
Divider(color: Colors.grey[300]),
SizedBox(height: 16),
Text("Status Event", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: darkBlue)),
SizedBox(height: 8),
Row(
children: [
Icon(Icons.play_circle_outline, size: 20, color: Colors.grey[700]),
SizedBox(width: 8),
Text(
displayEventStatusText, // Teks status baru
style: TextStyle(
fontSize: 15,
color: displayEventStatusColor, // Warna status baru
fontWeight: FontWeight.w600,
),
),
],
),
SizedBox(height: 4),
Row(
children: [
Icon(Icons.check_circle_outline, size: 20, color: Colors.grey[700]),
SizedBox(width: 8),
Text(
"Selesai Permanen: ${isEventFinishedPermanently ? 'Ya ✅' : 'Tidak ❌'}",
style: TextStyle(
fontSize: 15,
color: isEventFinishedPermanently ? Colors.green.shade700 : Colors.red.shade700,
fontWeight: FontWeight.w600,
),
),
],
),
SizedBox(height: 24),
// Tombol Mulai Event
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: (isEventStartedPermanently || isEventFinishedPermanently)
? null
: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('Mulai Event', style: TextStyle(color: darkBlue)),
content: Text('Peserta akan mulai scan RFID untuk 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('Mulai', style: TextStyle(color: Colors.green))),
],
),
);
if (confirm == true) {
final currentEventData = await docRef.get();
final currentTotalLaps = (currentEventData.data() as Map<String, dynamic>?)?['total_laps'] ?? 0;
await docRef.update({
'is_started_permanently': true,
'is_finished_permanently': false,
'start_time': Timestamp.now(),
}).catchError((e) {
print("❌ Gagal update Firestore: $e");
});
await _eventStatusRef.set({
'started': true,
'scan_mode': true,
'timestamp': ServerValue.timestamp,
'total_laps_target': currentTotalLaps,
'finished': false,
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Event dimulai. Peserta dapat scan RFID sekarang.')),
);
}
},
icon: Icon(Icons.play_arrow, color: Colors.white),
label: Text(
'Mulai Event (Scan RFID)',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: (isEventStartedPermanently || isEventFinishedPermanently) ? Colors.grey : Colors.green.shade600,
padding: EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 3,
),
),
),
// Tombol Akhiri Event Manual
if (isEventStartedPermanently && !isEventFinishedPermanently)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: Text('Akhiri Event', style: TextStyle(color: darkBlue)),
content: Text('Apakah Anda yakin ingin mengakhiri event ini secara manual? Status event akan permanen selesai dan data Realtime akan dihapus.', 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('Akhiri', style: TextStyle(color: Colors.red))),
],
),
);
if (confirm == true) {
await docRef.update({
'is_started_permanently': false,
'is_finished_permanently': true,
});
await _rtdb.ref('event_activity_data/${widget.eventId}').remove();
await _rtdb.ref('event_status/${widget.eventId}').remove();
await _rtdb.ref('scan_result/${widget.eventId}').remove();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Event telah diakhiri secara manual dan disimpan secara permanen.')),
);
}
},
icon: Icon(Icons.stop, color: Colors.white),
label: Text(
'Akhiri Event Manual',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
padding: EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 3,
),
),
),
),
],
),
),
),
SizedBox(height: 24),
Text("Peserta Event:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: darkBlue)),
SizedBox(height: 1),
],
),
),
),
// Daftar Peserta
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
sliver: ((firestoreData['participants'] as Map<String, dynamic>?)?.entries.isNotEmpty ?? false)
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final participantEntry = (firestoreData['participants'] as Map<String, dynamic>).entries.toList()[index];
final participant = participantEntry.value;
final name = participant['name'] ?? 'Nama tidak ditemukan';
final rfidUid = participant['rfid_uid'] as String?;
bool participantFinishedDisplay;
int participantLapsDisplay;
String participantTotalTimeDisplay = participant['total_time'] ?? '-';
// Prioritaskan data live (RTDB) jika event sedang berjalan (permanen)
if (isEventStartedPermanently && !isEventFinishedPermanently && _liveParticipantFinishedStatus.containsKey(rfidUid)) {
participantFinishedDisplay = _liveParticipantFinishedStatus[rfidUid]!;
} else {
participantFinishedDisplay = participant['finished'] ?? false; // Ambil dari Firestore
}
if (isEventStartedPermanently && !isEventFinishedPermanently && _liveParticipantCurrentLaps.containsKey(rfidUid)) {
participantLapsDisplay = _liveParticipantCurrentLaps[rfidUid]! > 0 ? _liveParticipantCurrentLaps[rfidUid]! - 1 : 0;
} else {
participantLapsDisplay = participant['laps'] ?? 0; // Ambil dari Firestore
}
return Card(
margin: EdgeInsets.symmetric(vertical: 8),
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: Colors.white, // CARD PUTIH
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: darkBlue,
),
),
SizedBox(height: 8),
_buildParticipantInfoRow(Icons.credit_card, "UID RFID", rfidUid ?? 'N/A'),
_buildParticipantInfoRow(Icons.sports_score, "Putaran", "$participantLapsDisplay"),
_buildParticipantInfoRow(
Icons.flag,
"Selesai",
participantFinishedDisplay ? 'Ya ✅' : 'Tidak ❌',
valueColor: participantFinishedDisplay ? Colors.green.shade700 : Colors.red.shade700,
),
if (participantFinishedDisplay && participantTotalTimeDisplay != '-')
_buildParticipantInfoRow(
Icons.timer,
"Waktu Total",
participantTotalTimeDisplay,
valueColor: darkBlue,
),
],
),
),
);
},
childCount: (firestoreData['participants'] as Map<String, dynamic>).length,
),
)
: SliverToBoxAdapter(
child: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
"Belum ada peserta yang terdaftar untuk event ini.",
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
textAlign: TextAlign.center,
),
),
),
),
),
SliverToBoxAdapter(
child: SizedBox(height: 24), // Tambahan spasi di bagian bawah
),
],
);
},
),
);
}
// Helper widget untuk baris informasi event
Widget _buildInfoRow(IconData icon, String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: primaryBlue),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey[700]),
),
Text(
value,
style: TextStyle(fontSize: 15, color: Colors.grey[900]),
),
],
),
),
],
);
}
// Helper widget untuk baris informasi peserta
Widget _buildParticipantInfoRow(IconData icon, String label, String value, {Color? valueColor}) {
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Row(
children: [
Icon(icon, size: 16, color: Colors.grey[600]),
SizedBox(width: 8),
Text(
"$label: ",
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
),
Expanded(
child: Text(
value,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: valueColor ?? darkBlue),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}