From 137afc04fab27a77ab02ebdc5976b5a734279643 Mon Sep 17 00:00:00 2001 From: Jishnu Chauhan Date: Thu, 7 Aug 2025 00:44:57 +0530 Subject: [PATCH 1/6] implemented complete player management with existing players and add functionality . --- .../organization/manage_players_screen.dart | 418 +++++++++++++++++- 1 file changed, 415 insertions(+), 3 deletions(-) diff --git a/lib/views/screens/organization/manage_players_screen.dart b/lib/views/screens/organization/manage_players_screen.dart index a332a96..f271da0 100644 --- a/lib/views/screens/organization/manage_players_screen.dart +++ b/lib/views/screens/organization/manage_players_screen.dart @@ -1,13 +1,425 @@ import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../../../models/user_model.dart'; +import 'add_players_screen.dart'; -class ManagePlayersScreen extends StatelessWidget { +class ManagePlayersScreen extends StatefulWidget { const ManagePlayersScreen({super.key}); + @override + State createState() => _ManagePlayersScreenState(); +} + +class _ManagePlayersScreenState extends State { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final FirebaseAuth _auth = FirebaseAuth.instance; + String? organizationSport; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _loadOrganizationSport(); + } + + Future _loadOrganizationSport() async { + try { + final uid = _auth.currentUser?.uid; + if (uid == null) return; + + final doc = await _firestore.collection('users').doc(uid).get(); + if (doc.exists) { + final userData = doc.data() as Map; + setState(() { + organizationSport = userData['sport']; + isLoading = false; + }); + } + } catch (e) { + setState(() { + isLoading = false; + }); + } + } + + Stream> _getConnectedPlayers() { + final uid = _auth.currentUser?.uid; + if (uid == null || organizationSport == null) { + return const Stream.empty(); + } + + return _firestore + .collection('organization_players') + .doc(uid) + .collection('connected_players') + .snapshots() + .asyncMap((snapshot) async { + List players = []; + for (var doc in snapshot.docs) { + try { + final playerDoc = await _firestore + .collection('users') + .doc(doc.id) + .get(); + if (playerDoc.exists) { + players.add(UserModel.fromFirestore(playerDoc)); + } + } catch (e) { + print('Error loading player: $e'); + } + } + return players; + }); + } + + Future _removePlayer(String playerId, String playerName) async { + try { + final uid = _auth.currentUser?.uid; + if (uid == null) return; + + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Remove Player'), + content: Text('Are you sure you want to remove $playerName from your organization?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Remove'), + ), + ], + ), + ); + + if (confirmed == true) { + await _firestore + .collection('organization_players') + .doc(uid) + .collection('connected_players') + .doc(playerId) + .delete(); + + await _firestore + .collection('player_organizations') + .doc(playerId) + .collection('connected_organizations') + .doc(uid) + .delete(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$playerName removed successfully')), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Error removing player')), + ); + } + } + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("Manage Players")), - body: const Center(child: Text("Manage Players Screen")), + appBar: AppBar( + title: const Text("Manage Players"), + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + ), + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : organizationSport == null + ? const Center( + child: Text( + 'Organization sport not found', + style: TextStyle(fontSize: 16), + ), + ) + : Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + color: Theme.of(context).primaryColor.withOpacity(0.1), + child: Row( + children: [ + Icon( + Icons.sports, + color: Theme.of(context).primaryColor, + ), + const SizedBox(width: 8), + Text( + 'Sport: $organizationSport', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryColor, + ), + ), + const Spacer(), + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddPlayersScreen( + organizationSport: organizationSport!, + ), + ), + ); + }, + icon: const Icon(Icons.add), + label: const Text('Add Players'), + ), + ], + ), + ), + Expanded( + child: StreamBuilder>( + stream: _getConnectedPlayers(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center( + child: Text('Error: ${snapshot.error}'), + ); + } + + final players = snapshot.data ?? []; + + if (players.isEmpty) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.group_off, + size: 80, + color: Colors.grey, + ), + SizedBox(height: 16), + Text( + 'No players connected yet', + style: TextStyle( + fontSize: 18, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8), + Text( + 'Add players to start tracking their performance', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: players.length, + itemBuilder: (context, index) { + final player = players[index]; + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + leading: CircleAvatar( + backgroundColor: Theme.of(context).primaryColor, + child: Text( + player.name.isNotEmpty ? player.name[0].toUpperCase() : 'P', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + title: Text( + player.name, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(player.email), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.sports, + size: 16, + color: Colors.grey[600], + ), + const SizedBox(width: 4), + Text( + player.sport, + style: TextStyle( + color: Colors.grey[600], + fontSize: 12, + ), + ), + ], + ), + ], + ), + trailing: PopupMenuButton( + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'view', + child: Row( + children: [ + Icon(Icons.visibility), + SizedBox(width: 8), + Text('View Profile'), + ], + ), + ), + const PopupMenuItem( + value: 'remove', + child: Row( + children: [ + Icon(Icons.remove_circle, color: Colors.red), + SizedBox(width: 8), + Text('Remove', style: TextStyle(color: Colors.red)), + ], + ), + ), + ], + onSelected: (value) { + if (value == 'view') { + _showPlayerDetails(player); + } else if (value == 'remove') { + _removePlayer(player.uid, player.name); + } + }, + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddPlayersScreen( + organizationSport: organizationSport!, + ), + ), + ); + }, + icon: const Icon(Icons.add), + label: const Text('Add Players'), + ), + ); + } + + void _showPlayerDetails(UserModel player) { + showDialog( + context: context, + builder: (context) => Dialog( + child: Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + radius: 30, + backgroundColor: Theme.of(context).primaryColor, + child: Text( + player.name.isNotEmpty ? player.name[0].toUpperCase() : 'P', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + player.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + player.email, + style: TextStyle( + color: Colors.grey[600], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + _buildDetailRow('Sport', player.sport), + _buildDetailRow('Role', player.role), + _buildDetailRow('Date of Birth', player.dob.toString().split(' ')[0]), + _buildDetailRow('Email Verified', player.emailVerified ? 'Yes' : 'No'), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 100, + child: Text( + '$label:', + style: const TextStyle(fontWeight: FontWeight.w500), + ), + ), + Expanded( + child: Text(value), + ), + ], + ), ); } } From 3a41e7fff5b329af42b0b07e50178d05892dca6b Mon Sep 17 00:00:00 2001 From: Jishnu Chauhan Date: Thu, 7 Aug 2025 00:47:00 +0530 Subject: [PATCH 2/6] Created screen for adding new players --- .../organization/add_players_screen.dart | 516 ++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 lib/views/screens/organization/add_players_screen.dart diff --git a/lib/views/screens/organization/add_players_screen.dart b/lib/views/screens/organization/add_players_screen.dart new file mode 100644 index 0000000..f13af35 --- /dev/null +++ b/lib/views/screens/organization/add_players_screen.dart @@ -0,0 +1,516 @@ +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../../../models/user_model.dart'; + +class AddPlayersScreen extends StatefulWidget { + final String organizationSport; + + const AddPlayersScreen({ + super.key, + required this.organizationSport, + }); + + @override + State createState() => _AddPlayersScreenState(); +} + +class _AddPlayersScreenState extends State { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final FirebaseAuth _auth = FirebaseAuth.instance; + final TextEditingController _searchController = TextEditingController(); + String _searchQuery = ''; + Set _connectedPlayerIds = {}; + Set _pendingRequestIds = {}; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _loadConnectedPlayersAndRequests(); + } + + Future _loadConnectedPlayersAndRequests() async { + try { + final uid = _auth.currentUser?.uid; + if (uid == null) return; + + // Load connected players + final connectedSnapshot = await _firestore + .collection('organization_players') + .doc(uid) + .collection('connected_players') + .get(); + + // Load pending requests + final requestsSnapshot = await _firestore + .collection('connection_requests') + .where('organizationId', isEqualTo: uid) + .where('status', isEqualTo: 'pending') + .get(); + + setState(() { + _connectedPlayerIds = connectedSnapshot.docs.map((doc) => doc.id).toSet(); + _pendingRequestIds = requestsSnapshot.docs.map((doc) => doc.data()['athleteId'] as String).toSet(); + isLoading = false; + }); + } catch (e) { + setState(() { + isLoading = false; + }); + } + } + + Stream> _getAvailablePlayers() { + return _firestore + .collection('users') + .where('role', isEqualTo: 'Athlete') + .where('sport', isEqualTo: widget.organizationSport) + .snapshots() + .map((snapshot) { + List players = []; + for (var doc in snapshot.docs) { + try { + final player = UserModel.fromFirestore(doc); + // Filter based on search query and exclude connected players + if (!_connectedPlayerIds.contains(player.uid)) { + if (_searchQuery.isEmpty || + player.name.toLowerCase().contains(_searchQuery.toLowerCase()) || + player.email.toLowerCase().contains(_searchQuery.toLowerCase())) { + players.add(player); + } + } + } catch (e) { + print('Error parsing player: $e'); + } + } + return players; + }); + } + + Future _sendConnectionRequest(UserModel player) async { + try { + final uid = _auth.currentUser?.uid; + if (uid == null) return; + + // Get organization details + final orgDoc = await _firestore.collection('users').doc(uid).get(); + final orgData = orgDoc.data() as Map; + + // Create connection request + await _firestore.collection('connection_requests').add({ + 'organizationId': uid, + 'organizationName': orgData['name'], + 'athleteId': player.uid, + 'athleteName': player.name, + 'sport': widget.organizationSport, + 'status': 'pending', + 'createdAt': FieldValue.serverTimestamp(), + }); + + // Create notification for the athlete + await _firestore + .collection('notifications') + .doc(player.uid) + .collection('user_notifications') + .add({ + 'title': 'Connection Request', + 'message': '${orgData['name']} wants to connect with you', + 'type': 'connection_request', + 'organizationId': uid, + 'createdAt': FieldValue.serverTimestamp(), + 'read': false, + }); + + setState(() { + _pendingRequestIds.add(player.uid); + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Connection request sent to ${player.name}'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error sending connection request'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Add Players"), + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + ), + body: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + color: Theme.of(context).primaryColor.withOpacity(0.1), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.sports, + color: Theme.of(context).primaryColor, + ), + const SizedBox(width: 8), + Text( + 'Finding ${widget.organizationSport} Athletes', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryColor, + ), + ), + ], + ), + const SizedBox(height: 12), + TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Search athletes by name or email...', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), + filled: true, + fillColor: Colors.white, + ), + onChanged: (value) { + setState(() { + _searchQuery = value; + }); + }, + ), + ], + ), + ), + Expanded( + child: StreamBuilder>( + stream: _getAvailablePlayers(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center( + child: Text('Error: ${snapshot.error}'), + ); + } + + final players = snapshot.data ?? []; + + if (players.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + size: 80, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + _searchQuery.isEmpty + ? 'No available ${widget.organizationSport} athletes found' + : 'No athletes match your search', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + if (_searchQuery.isNotEmpty) ...[ + const SizedBox(height: 8), + TextButton( + onPressed: () { + _searchController.clear(); + setState(() { + _searchQuery = ''; + }); + }, + child: const Text('Clear search'), + ), + ], + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: players.length, + itemBuilder: (context, index) { + final player = players[index]; + final isPending = _pendingRequestIds.contains(player.uid); + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + leading: CircleAvatar( + backgroundColor: Theme.of(context).primaryColor, + child: Text( + player.name.isNotEmpty ? player.name[0].toUpperCase() : 'A', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + title: Text( + player.name, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(player.email), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.sports, + size: 16, + color: Colors.grey[600], + ), + const SizedBox(width: 4), + Text( + player.sport, + style: TextStyle( + color: Colors.grey[600], + fontSize: 12, + ), + ), + const SizedBox(width: 12), + if (player.emailVerified) ...[ + const Icon( + Icons.verified, + size: 16, + color: Colors.green, + ), + const SizedBox(width: 4), + const Text( + 'Verified', + style: TextStyle( + color: Colors.green, + fontSize: 12, + ), + ), + ], + ], + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => _showPlayerDetails(player), + icon: const Icon(Icons.visibility), + tooltip: 'View Details', + ), + if (isPending) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.orange), + ), + child: const Text( + 'Pending', + style: TextStyle( + color: Colors.orange, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ) + else + IconButton( + onPressed: () => _sendConnectionRequest(player), + icon: const Icon(Icons.add_circle), + color: Colors.green, + tooltip: 'Send Request', + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } + + void _showPlayerDetails(UserModel player) { + showDialog( + context: context, + builder: (context) => Dialog( + child: Container( + padding: const EdgeInsets.all(20), + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + radius: 30, + backgroundColor: Theme.of(context).primaryColor, + child: Text( + player.name.isNotEmpty ? player.name[0].toUpperCase() : 'A', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + player.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + player.email, + style: TextStyle( + color: Colors.grey[600], + ), + ), + if (player.emailVerified) + const Row( + children: [ + Icon( + Icons.verified, + size: 16, + color: Colors.green, + ), + SizedBox(width: 4), + Text( + 'Verified', + style: TextStyle( + color: Colors.green, + fontSize: 12, + ), + ), + ], + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + _buildDetailRow('Sport', player.sport), + _buildDetailRow('Role', player.role), + _buildDetailRow('Date of Birth', player.dob.toString().split(' ')[0]), + _buildDetailRow('Account Created', player.createdAt.toString().split(' ')[0]), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + if (!_pendingRequestIds.contains(player.uid)) + ElevatedButton.icon( + onPressed: () { + Navigator.of(context).pop(); + _sendConnectionRequest(player); + }, + icon: const Icon(Icons.add), + label: const Text('Send Request'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ) + else + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.orange), + ), + child: const Text( + 'Request Pending', + style: TextStyle( + color: Colors.orange, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + '$label:', + style: const TextStyle(fontWeight: FontWeight.w500), + ), + ), + Expanded( + child: Text(value), + ), + ], + ), + ); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } +} From 21f1f488b6672eda4d5273d1c63afca75ad474c1 Mon Sep 17 00:00:00 2001 From: Jishnu Chauhan Date: Thu, 7 Aug 2025 00:48:46 +0530 Subject: [PATCH 3/6] Created Service for handling Connection Request . --- lib/services/connection_service.dart | 193 +++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 lib/services/connection_service.dart diff --git a/lib/services/connection_service.dart b/lib/services/connection_service.dart new file mode 100644 index 0000000..3cbc637 --- /dev/null +++ b/lib/services/connection_service.dart @@ -0,0 +1,193 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; + +class ConnectionService { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final FirebaseAuth _auth = FirebaseAuth.instance; + + /// Get pending connection requests for an athlete + Stream>> getConnectionRequests() { + final uid = _auth.currentUser?.uid; + if (uid == null) return const Stream.empty(); + + return _firestore + .collection('connection_requests') + .where('athleteId', isEqualTo: uid) + .where('status', isEqualTo: 'pending') + .orderBy('createdAt', descending: true) + .snapshots() + .map((snapshot) => snapshot.docs.map((doc) { + final data = doc.data(); + data['id'] = doc.id; + return data; + }).toList()); + } + + /// Accept a connection request + Future acceptConnectionRequest(String requestId, Map requestData) async { + try { + final uid = _auth.currentUser?.uid; + if (uid == null) return false; + + final batch = _firestore.batch(); + + // Update request status + batch.update( + _firestore.collection('connection_requests').doc(requestId), + {'status': 'accepted', 'updatedAt': FieldValue.serverTimestamp()}, + ); + + // Add player to organization's connected players + batch.set( + _firestore + .collection('organization_players') + .doc(requestData['organizationId']) + .collection('connected_players') + .doc(uid), + { + 'connectedAt': FieldValue.serverTimestamp(), + 'status': 'active', + }, + ); + + // Add organization to player's connected organizations + batch.set( + _firestore + .collection('player_organizations') + .doc(uid) + .collection('connected_organizations') + .doc(requestData['organizationId']), + { + 'connectedAt': FieldValue.serverTimestamp(), + 'status': 'active', + }, + ); + + // Create notification for organization + batch.add( + _firestore + .collection('notifications') + .doc(requestData['organizationId']) + .collection('user_notifications'), + { + 'title': 'Connection Accepted', + 'message': '${requestData['athleteName']} accepted your connection request', + 'type': 'connection_accepted', + 'athleteId': uid, + 'createdAt': FieldValue.serverTimestamp(), + 'read': false, + }, + ); + + await batch.commit(); + return true; + } catch (e) { + print('Error accepting connection request: $e'); + return false; + } + } + + /// Reject a connection request + Future rejectConnectionRequest(String requestId, Map requestData) async { + try { + final uid = _auth.currentUser?.uid; + if (uid == null) return false; + + final batch = _firestore.batch(); + + // Update request status + batch.update( + _firestore.collection('connection_requests').doc(requestId), + {'status': 'rejected', 'updatedAt': FieldValue.serverTimestamp()}, + ); + + // Create notification for organization + batch.add( + _firestore + .collection('notifications') + .doc(requestData['organizationId']) + .collection('user_notifications'), + { + 'title': 'Connection Rejected', + 'message': '${requestData['athleteName']} rejected your connection request', + 'type': 'connection_rejected', + 'athleteId': uid, + 'createdAt': FieldValue.serverTimestamp(), + 'read': false, + }, + ); + + await batch.commit(); + return true; + } catch (e) { + print('Error rejecting connection request: $e'); + return false; + } + } + + /// Get connected organizations for an athlete + Stream>> getConnectedOrganizations() { + final uid = _auth.currentUser?.uid; + if (uid == null) return const Stream.empty(); + + return _firestore + .collection('player_organizations') + .doc(uid) + .collection('connected_organizations') + .snapshots() + .asyncMap((snapshot) async { + List> organizations = []; + for (var doc in snapshot.docs) { + try { + final orgDoc = await _firestore + .collection('users') + .doc(doc.id) + .get(); + if (orgDoc.exists) { + final orgData = orgDoc.data() as Map; + orgData['id'] = doc.id; + orgData['connectedAt'] = doc.data()['connectedAt']; + organizations.add(orgData); + } + } catch (e) { + print('Error loading organization: $e'); + } + } + return organizations; + }); + } + + /// Disconnect from an organization + Future disconnectFromOrganization(String organizationId) async { + try { + final uid = _auth.currentUser?.uid; + if (uid == null) return false; + + final batch = _firestore.batch(); + + // Remove from organization's connected players + batch.delete( + _firestore + .collection('organization_players') + .doc(organizationId) + .collection('connected_players') + .doc(uid), + ); + + // Remove from player's connected organizations + batch.delete( + _firestore + .collection('player_organizations') + .doc(uid) + .collection('connected_organizations') + .doc(organizationId), + ); + + await batch.commit(); + return true; + } catch (e) { + print('Error disconnecting from organization: $e'); + return false; + } + } +} From ac9773a28a8f1ea700776fd454acfce6b9323d61 Mon Sep 17 00:00:00 2001 From: Jishnu Chauhan Date: Thu, 7 Aug 2025 00:52:18 +0530 Subject: [PATCH 4/6] Create screen For Athlete to view and respond to connection Requests . --- .../athlete/connection_requests_screen.dart | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 lib/views/screens/athlete/connection_requests_screen.dart diff --git a/lib/views/screens/athlete/connection_requests_screen.dart b/lib/views/screens/athlete/connection_requests_screen.dart new file mode 100644 index 0000000..1ba026e --- /dev/null +++ b/lib/views/screens/athlete/connection_requests_screen.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import '../../../services/connection_service.dart'; + +class ConnectionRequestsScreen extends StatefulWidget { + const ConnectionRequestsScreen({super.key}); + + @override + State createState() => _ConnectionRequestsScreenState(); +} + +class _ConnectionRequestsScreenState extends State { + final ConnectionService _connectionService = ConnectionService(); + + Future _handleRequest(String requestId, Map requestData, bool accept) async { + final success = accept + ? await _connectionService.acceptConnectionRequest(requestId, requestData) + : await _connectionService.rejectConnectionRequest(requestId, requestData); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + success + ? '${accept ? 'Accepted' : 'Rejected'} connection request' + : 'Error processing request', + ), + backgroundColor: success ? Colors.green : Colors.red, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Connection Requests"), + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + ), + body: StreamBuilder>>( + stream: _connectionService.getConnectionRequests(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center( + child: Text('Error: ${snapshot.error}'), + ); + } + + final requests = snapshot.data ?? []; + + if (requests.isEmpty) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inbox, + size: 80, + color: Colors.grey, + ), + SizedBox(height: 16), + Text( + 'No connection requests', + style: TextStyle( + fontSize: 18, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: requests.length, + itemBuilder: (context, index) { + final request = requests[index]; + final createdAt = request['createdAt']?.toDate(); + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + backgroundColor: Theme.of(context).primaryColor, + child: Text( + request['organizationName']?[0]?.toUpperCase() ?? 'O', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + request['organizationName'] ?? 'Unknown Organization', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'wants to connect with you', + style: TextStyle( + color: Colors.grey[600], + ), + ), + if (createdAt != null) + Text( + 'Received: ${createdAt.day}/${createdAt.month}/${createdAt.year}', + style: TextStyle( + color: Colors.grey[500], + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + if (request['sport'] != null) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + request['sport'], + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => _handleRequest(request['id'], request, false), + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('Reject'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () => _handleRequest(request['id'], request, true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + child: const Text('Accept'), + ), + ], + ), + ], + ), + ), + ); + }, + ); + }, + ), + ); + } +} From 3af2401361d8968a463eb84354c4b4b5f610e185 Mon Sep 17 00:00:00 2001 From: Jishnu Chauhan Date: Thu, 7 Aug 2025 00:53:58 +0530 Subject: [PATCH 5/6] Added Connection Request Navigation --- .../screens/athlete/athlete_dashboard.dart | 66 +++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/lib/views/screens/athlete/athlete_dashboard.dart b/lib/views/screens/athlete/athlete_dashboard.dart index 495f126..e0b74d1 100644 --- a/lib/views/screens/athlete/athlete_dashboard.dart +++ b/lib/views/screens/athlete/athlete_dashboard.dart @@ -361,7 +361,7 @@ class _DashboardScreenState extends State style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: Color(0xFF667EEA), + color: const Color(0xFF667EEA), ), ), ], @@ -396,13 +396,19 @@ class _DashboardScreenState extends State ), const SizedBox(height: 20), GridView.count( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), crossAxisCount: 2, + padding: const EdgeInsets.all(16), crossAxisSpacing: 16, mainAxisSpacing: 16, childAspectRatio: 1.1, children: [ + _buildDashboardCard( + context, + 'Connection Requests', + Icons.group_add, + Colors.orange, + () => Navigator.pushNamed(context, '/connection-requests'), + ), _buildEnhancedActionCard( icon: Icons.healing_rounded, label: "Injury Tracker", @@ -646,4 +652,56 @@ class _DashboardScreenState extends State final uid = FirebaseAuth.instance.currentUser!.uid; return await FirebaseFirestore.instance.collection('users').doc(uid).get(); } -} \ No newline at end of file + + Widget _buildDashboardCard( + BuildContext context, + String title, + IconData icon, + Color color, + VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Icon( + icon, + size: 32, + color: color, + ), + ), + const SizedBox(height: 12), + Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: color, + ), + ), + ], + ), + ), + ); + } +} From dccbac5b032f7fed9118124a90a2798028178455 Mon Sep 17 00:00:00 2001 From: Jishnu Chauhan Date: Thu, 7 Aug 2025 00:56:15 +0530 Subject: [PATCH 6/6] Added Connection Requests Route . --- lib/main.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 0ed4634..eff4ad4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import 'views/screens/splash_screen.dart'; +import 'views/screens/connection_requests_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -22,6 +23,10 @@ class MyApp extends StatelessWidget { fontFamily: 'Montserrat', ), home: const SplashScreen(), + routes: { + '/': (context) => const SplashScreen(), + '/connection-requests': (context) => const ConnectionRequestsScreen(), + }, ); } }