816 lines
38 KiB
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |