import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:tugas_akhir_supabase/core/theme/app_colors.dart'; import 'package:tugas_akhir_supabase/screens/community/models/group.dart'; class GroupMember { final String userId; final String username; final String? avatarUrl; final String role; final DateTime joinedAt; GroupMember({ required this.userId, required this.username, this.avatarUrl, required this.role, required this.joinedAt, }); factory GroupMember.fromMap(Map map) { return GroupMember( userId: map['user_id'], username: map['username'] ?? 'Unknown User', avatarUrl: map['avatar_url'], role: map['role'] ?? 'member', joinedAt: map['joined_at'] != null ? DateTime.parse(map['joined_at']) : DateTime.now(), ); } } class NonMember { final String userId; final String username; final String? avatarUrl; final String email; NonMember({ required this.userId, required this.username, this.avatarUrl, required this.email, }); factory NonMember.fromMap(Map map) { return NonMember( userId: map['user_id'], username: map['username'] ?? 'Unknown User', avatarUrl: map['avatar_url'], email: map['email'] ?? '', ); } } class GroupDetailDialog extends StatefulWidget { final Group group; final Function() onGroupUpdated; const GroupDetailDialog({ super.key, required this.group, required this.onGroupUpdated, }); @override State createState() => _GroupDetailDialogState(); } class _GroupDetailDialogState extends State with SingleTickerProviderStateMixin { late TabController _tabController; bool _isLoading = true; List _members = []; List _nonMembers = []; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _loadGroupMembers(); } @override void dispose() { _tabController.dispose(); super.dispose(); } Future _loadGroupMembers() async { if (!mounted) return; setState(() => _isLoading = true); try { final supabase = Supabase.instance.client; // Load group members final membersResponse = await supabase.rpc( 'get_group_members', params: {'group_id_param': widget.group.id}, ); if (!mounted) return; List members = []; if (membersResponse != null) { for (final item in membersResponse) { members.add(GroupMember.fromMap(item)); } } // Load non-members final nonMembersResponse = await supabase.rpc( 'get_users_not_in_group', params: {'group_id_param': widget.group.id}, ); if (!mounted) return; List nonMembers = []; if (nonMembersResponse != null) { for (final item in nonMembersResponse) { nonMembers.add(NonMember.fromMap(item)); } } if (mounted) { setState(() { _members = members; _nonMembers = nonMembers; _isLoading = false; }); } } catch (e) { print('[ERROR] Failed to load group members: $e'); if (mounted) { setState(() => _isLoading = false); _showErrorSnackBar( 'Failed to load group members: ${e.toString().split('Exception:').last.trim()}', ); } } } Future _removeUserFromGroup(String userId) async { if (!mounted) return; setState(() => _isLoading = true); try { final supabase = Supabase.instance.client; final result = await supabase.rpc( 'remove_user_from_group', params: {'user_id_param': userId, 'group_id_param': widget.group.id}, ); if (!mounted) return; if (result == true) { _showSuccessSnackBar('User removed from group successfully'); await _loadGroupMembers(); // Reload members widget.onGroupUpdated(); // Refresh parent } else { _showErrorSnackBar('Failed to remove user from group'); setState(() => _isLoading = false); } } catch (e) { print('[ERROR] Failed to remove user from group: $e'); if (mounted) { _showErrorSnackBar( 'Error: ${e.toString().split('Exception:').last.trim()}', ); setState(() => _isLoading = false); } } } Future _addUserToGroup(String userId) async { if (!mounted) return; setState(() => _isLoading = true); try { final supabase = Supabase.instance.client; final result = await supabase.rpc( 'add_user_to_group', params: { 'user_id_param': userId, 'group_id_param': widget.group.id, 'role_param': 'member', }, ); if (!mounted) return; if (result == true) { _showSuccessSnackBar('User added to group successfully'); await _loadGroupMembers(); // Reload members widget.onGroupUpdated(); // Refresh parent } else { _showErrorSnackBar('Failed to add user to group'); setState(() => _isLoading = false); } } catch (e) { print('[ERROR] Failed to add user to group: $e'); if (mounted) { _showErrorSnackBar( 'Error: ${e.toString().split('Exception:').last.trim()}', ); setState(() => _isLoading = false); } } } void _showErrorSnackBar(String message) { if (!mounted) return; final scaffoldMessenger = ScaffoldMessenger.of(context); scaffoldMessenger.clearSnackBars(); scaffoldMessenger.showSnackBar( SnackBar(content: Text(message), backgroundColor: Colors.red), ); } void _showSuccessSnackBar(String message) { if (!mounted) return; final scaffoldMessenger = ScaffoldMessenger.of(context); scaffoldMessenger.clearSnackBars(); scaffoldMessenger.showSnackBar( SnackBar(content: Text(message), backgroundColor: Colors.green), ); } void _showRemoveConfirmation(GroupMember member) { if (widget.group.isDefault) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Remove from Default Group?'), content: Text( 'This is a default group. "${member.username}" will be temporarily removed but will rejoin if default group settings change.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), ElevatedButton( onPressed: () { Navigator.pop(context); _removeUserFromGroup(member.userId); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('Remove'), ), ], ), ); } else { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Remove Member?'), content: Text( 'Are you sure you want to remove "${member.username}" from this group?', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), ElevatedButton( onPressed: () { Navigator.pop(context); _removeUserFromGroup(member.userId); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('Remove'), ), ], ), ); } } @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( width: double.maxFinite, constraints: BoxConstraints( maxWidth: 600, maxHeight: MediaQuery.of(context).size.height * 0.8, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header with group info Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.primary, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( backgroundColor: Colors.white.withOpacity(0.3), child: Text( widget.group.name.substring(0, 1).toUpperCase(), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.group.name, style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), Row( children: [ if (widget.group.isDefault) Container( margin: const EdgeInsets.only( top: 4, right: 4, ), padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: Colors.white.withOpacity(0.3), borderRadius: BorderRadius.circular(12), ), child: const Text( 'Default Group', style: TextStyle( fontSize: 12, color: Colors.white, ), ), ), Text( '${_members.length} members', style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 14, ), ), ], ), ], ), ), IconButton( icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.pop(context), ), ], ), if (widget.group.description.isNotEmpty) ...[ const SizedBox(height: 8), Text( widget.group.description, style: TextStyle( color: Colors.white.withOpacity(0.9), fontSize: 14, ), ), ], ], ), ), // Tabs TabBar( controller: _tabController, labelColor: AppColors.primary, unselectedLabelColor: Colors.grey, tabs: const [Tab(text: 'Members'), Tab(text: 'Add Members')], ), // Tab content Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) : TabBarView( controller: _tabController, children: [ // Members tab _buildMembersTab(), // Add members tab _buildAddMembersTab(), ], ), ), ], ), ), ); } Widget _buildMembersTab() { if (_members.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.people, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'No members in this group', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey[700], ), ), const SizedBox(height: 8), Text( 'Add members from the "Add Members" tab', style: TextStyle(color: Colors.grey[600]), ), ], ), ); } return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: _members.length, itemBuilder: (context, index) { final member = _members[index]; return ListTile( leading: CircleAvatar( backgroundColor: AppColors.primary.withOpacity(0.2), backgroundImage: member.avatarUrl != null && member.avatarUrl!.isNotEmpty ? NetworkImage(member.avatarUrl!) : null, child: member.avatarUrl == null || member.avatarUrl!.isEmpty ? Text( member.username.substring(0, 1).toUpperCase(), style: const TextStyle( color: AppColors.primary, fontWeight: FontWeight.bold, ), ) : null, ), title: Text( member.username, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( 'Joined: ${_formatDate(member.joinedAt)} • ${member.role}', style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), trailing: IconButton( icon: const Icon(Icons.remove_circle_outline, color: Colors.red), onPressed: () => _showRemoveConfirmation(member), ), ); }, ); } Widget _buildAddMembersTab() { if (_nonMembers.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.people, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'All users are already members', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey[700], ), ), const SizedBox(height: 8), ElevatedButton.icon( icon: const Icon(Icons.refresh), label: const Text('Refresh'), onPressed: _loadGroupMembers, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, ), ), ], ), ); } return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: _nonMembers.length, itemBuilder: (context, index) { final nonMember = _nonMembers[index]; return ListTile( leading: CircleAvatar( backgroundColor: AppColors.primary.withOpacity(0.2), backgroundImage: nonMember.avatarUrl != null && nonMember.avatarUrl!.isNotEmpty ? NetworkImage(nonMember.avatarUrl!) : null, child: nonMember.avatarUrl == null || nonMember.avatarUrl!.isEmpty ? Text( nonMember.username.substring(0, 1).toUpperCase(), style: const TextStyle( color: AppColors.primary, fontWeight: FontWeight.bold, ), ) : null, ), title: Text( nonMember.username, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( nonMember.email, style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), trailing: IconButton( icon: const Icon(Icons.add_circle_outline, color: Colors.green), onPressed: () => _addUserToGroup(nonMember.userId), ), ); }, ); } String _formatDate(DateTime date) { final now = DateTime.now(); final difference = now.difference(date); if (difference.inDays < 1) { return 'Today'; } else if (difference.inDays < 2) { return 'Yesterday'; } else if (difference.inDays < 7) { return '${difference.inDays} days ago'; } else { return '${date.day}/${date.month}/${date.year}'; } } }