import 'package:flutter/material.dart'; import 'package:tugas_akhir_supabase/core/theme/app_colors.dart'; import 'package:tugas_akhir_supabase/screens/community/models/group.dart'; import 'package:tugas_akhir_supabase/screens/community/services/group_management_service.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'dart:convert'; import 'package:tugas_akhir_supabase/screens/admin/group_detail_dialog.dart'; import 'package:flutter/rendering.dart'; // Model untuk menampilkan pengguna dan grup yang mereka ikuti class UserWithGroups { final String userId; final String username; final String email; final String? avatarUrl; final bool isActive; final DateTime? lastSignIn; final List groups; UserWithGroups({ required this.userId, required this.username, required this.email, this.avatarUrl, required this.isActive, this.lastSignIn, required this.groups, }); } class UserGroup { final String groupId; final String groupName; final String role; UserGroup({ required this.groupId, required this.groupName, required this.role, }); } class CommunityManagement extends StatefulWidget { const CommunityManagement({super.key}); @override _CommunityManagementState createState() => _CommunityManagementState(); } class _CommunityManagementState extends State with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { final _groupManagementService = GroupManagementService(); late TabController _tabController; // State variables bool _isLoading = true; List _groups = []; List _users = []; bool _isLoadingUsers = true; // Text controllers for group creation/editing final _groupNameController = TextEditingController(); final _groupDescriptionController = TextEditingController(); bool _isPublicGroup = true; bool _isDefaultGroup = true; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _loadGroups(); _loadUsers(); // Load users data } @override void dispose() { _tabController.dispose(); _groupNameController.dispose(); _groupDescriptionController.dispose(); super.dispose(); } @override bool get wantKeepAlive => true; Future _loadGroups() async { print('[DEBUG] _loadGroups called'); if (!mounted) return; setState(() => _isLoading = true); try { print('[DEBUG] Admin: Loading all groups directly from database...'); // Gunakan pendekatan langsung tanpa mengandalkan service yang mungkin terpengaruh RLS final supabase = Supabase.instance.client; final response = await supabase.rpc('get_all_groups_for_admin'); if (!mounted) return; print('[DEBUG] Admin: Raw response: $response'); List groups = []; if (response != null) { for (final item in response) { try { // The RPC now returns member_count directly final group = Group.fromMap(item); groups.add(group); print( '[DEBUG] Admin: Added group: ${group.name} (${group.id}) with ${group.memberCount} members', ); } catch (e) { print('[ERROR] Admin: Failed to parse group: $e'); } } } print( '[DEBUG] Admin: Loaded ${groups.length} groups directly from database', ); if (mounted) { setState(() { _groups = groups; _isLoading = false; }); print('[DEBUG] Admin: State updated with ${_groups.length} groups'); } } catch (e) { print('[ERROR] Admin: Failed to load groups: $e'); if (mounted) { setState(() => _isLoading = false); _showErrorSnackBar('Failed to load groups. Please try again.'); } } // Safety timer untuk mengatasi kemungkinan loading yang tidak berhenti Future.delayed(Duration(seconds: 5), () { if (mounted && _isLoading) { print('[WARNING] Admin: Forcing load completion after timeout'); setState(() => _isLoading = false); } }); } Future _loadUsers() async { if (!mounted) return; setState(() => _isLoadingUsers = true); try { print('[DEBUG] Admin: Loading all users with their groups...'); // Menggunakan fungsi yang mengembalikan pengguna beserta grup final supabase = Supabase.instance.client; // Coba menggunakan fungsi get_users_with_groups dynamic response; try { // Sinkronkan pengguna ke grup default terlebih dahulu final syncResult = await supabase.rpc('sync_users_to_default_groups'); if (!mounted) return; int added = 0, already = 0, reactivated = 0; if (syncResult != null && syncResult is List) { for (final item in syncResult) { if (item['status'] == 'added_new') added++; if (item['status'] == 'already_member') already++; if (item['status'] == 'reactivated') reactivated++; } } print('[DEBUG] Admin: Synchronized users to default groups:'); print(' - Added: $added users'); print(' - Already members: $already users'); print(' - Reactivated: $reactivated users'); // Sekarang ambil data pengguna response = await supabase.rpc('get_users_with_groups'); if (!mounted) return; print( '[DEBUG] Admin: Successfully used get_users_with_groups function', ); } catch (e) { if (!mounted) return; print('[WARNING] Admin: get_users_with_groups function failed: $e'); // Fallback ke metode lama jika fungsi tidak tersedia response = await supabase.rpc('get_users_for_admin'); if (!mounted) return; print('[DEBUG] Admin: Fallback to get_users_for_admin function'); } print('[DEBUG] Admin: Raw users response: $response'); List users = []; if (response != null) { for (final dynamic item in response) { try { // Konversi item ke UserWithGroups Map userData; if (item is Map) { userData = Map.from(item); } else if (item is String) { // Jika dikembalikan sebagai JSON string userData = jsonDecode(item) as Map; } else { continue; // Skip item yang tidak valid } final userId = userData['user_id'] as String? ?? ''; if (userId.isEmpty) continue; // Skip jika user_id kosong // Mendapatkan grup yang diikuti pengguna List groups = []; if (userData.containsKey('groups') && userData['groups'] != null) { final groupsData = userData['groups']; dynamic groupsList; if (groupsData is String) { groupsList = jsonDecode(groupsData); } else { groupsList = groupsData; } if (groupsList is List) { for (final groupData in groupsList) { Map groupMap; if (groupData is Map) { groupMap = Map.from(groupData); } else if (groupData is String) { groupMap = jsonDecode(groupData) as Map; } else { continue; } groups.add( UserGroup( groupId: groupMap['group_id'] as String? ?? '', groupName: groupMap['group_name'] as String? ?? 'Unknown Group', role: groupMap['is_default'] == true ? 'default member' : 'member', ), ); } } } users.add( UserWithGroups( userId: userId, username: userData['username'] as String? ?? 'Unknown User', email: userData['email'] as String? ?? 'No Email', avatarUrl: userData['avatar_url'] as String?, isActive: userData['is_active'] as bool? ?? true, lastSignIn: null, // Mengabaikan last_sign_in untuk menghindari error groups: groups, ), ); } catch (e) { print('[ERROR] Admin: Failed to parse user data: $e'); } } } print('[DEBUG] Admin: Loaded ${users.length} users'); if (mounted) { setState(() { _users = users; _isLoadingUsers = false; }); print('[DEBUG] Admin: State updated with ${_users.length} users'); } } catch (e) { print('[ERROR] Admin: Failed to load users: $e'); if (mounted) { setState(() => _isLoadingUsers = false); _showErrorSnackBar( 'Failed to load users: ${e.toString().split('Exception:').last.trim()}', ); } } } Future _toggleUserStatus(String userId, bool newStatus) async { if (!mounted) return; setState(() => _isLoadingUsers = true); try { print('[DEBUG] Admin: Toggling user status: $userId to $newStatus'); // Panggil RPC untuk mengubah status pengguna final supabase = Supabase.instance.client; final result = await supabase.rpc( 'toggle_user_status', params: {'user_id_param': userId, 'is_active_param': newStatus}, ); if (!mounted) return; if (result == true) { _showSuccessSnackBar('User status updated successfully'); _loadUsers(); // Reload user list } else { _showErrorSnackBar('Failed to update user status'); setState(() => _isLoadingUsers = false); } } catch (e) { print('[ERROR] Admin: Failed to toggle user status: $e'); if (mounted) { _showErrorSnackBar( 'Error: ${e.toString().split('Exception:').last.trim()}', ); setState(() => _isLoadingUsers = false); } } } Future _removeUserFromGroup(String userId, String groupId) async { if (!mounted) return; setState(() => _isLoadingUsers = true); try { print('[DEBUG] Admin: Removing user $userId from group $groupId'); // Panggil RPC untuk menghapus pengguna dari grup final supabase = Supabase.instance.client; final result = await supabase.rpc( 'remove_user_from_group', params: {'user_id_param': userId, 'group_id_param': groupId}, ); if (!mounted) return; if (result == true) { _showSuccessSnackBar('User removed from group successfully'); _loadUsers(); // Reload user list } else { _showErrorSnackBar('Failed to remove user from group'); setState(() => _isLoadingUsers = false); } } catch (e) { print('[ERROR] Admin: Failed to remove user from group: $e'); if (mounted) { _showErrorSnackBar( 'Error: ${e.toString().split('Exception:').last.trim()}', ); setState(() => _isLoadingUsers = false); } } } void _showCreateGroupDialog() { // Reset form _groupNameController.text = ''; _groupDescriptionController.text = ''; _isPublicGroup = true; // Tetap true karena kita menghilangkan opsi ini _isDefaultGroup = false; // Default ke false untuk grup baru // Cek apakah sudah ada grup default bool hasDefaultGroup = _groups.any((group) => group.isDefault); showDialog( context: context, builder: (context) => StatefulBuilder( builder: (BuildContext context, StateSetter setDialogState) { return AlertDialog( title: const Text('Create New Group'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: _groupNameController, decoration: const InputDecoration( labelText: 'Group Name', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: _groupDescriptionController, decoration: const InputDecoration( labelText: 'Description', border: OutlineInputBorder(), ), maxLines: 3, ), const SizedBox(height: 16), Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Default Group', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), Text( 'All new users join automatically', style: TextStyle( color: Colors.grey[600], fontSize: 12, ), ), ], ), ), if (!hasDefaultGroup) Switch( value: _isDefaultGroup, activeColor: Colors.green, onChanged: (value) { setDialogState(() { _isDefaultGroup = value; }); }, ) else Tooltip( message: 'Default group already exists', child: Switch( value: false, activeColor: Colors.grey, onChanged: null, ), ), ], ), ), if (hasDefaultGroup) Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( children: [ Icon( Icons.info_outline, color: Colors.orange, size: 16, ), SizedBox(width: 8), Expanded( child: Text( 'Default group already exists', style: TextStyle( color: Colors.grey[600], fontStyle: FontStyle.italic, fontSize: 12, ), ), ), ], ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), ElevatedButton( onPressed: () { Navigator.pop(context); // Jika sudah ada grup default, pastikan grup baru tidak menjadi default if (hasDefaultGroup) { _isDefaultGroup = false; } _createGroup(); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, ), child: const Text('Create'), ), ], ); }, ), ); } Future _createGroup() async { final name = _groupNameController.text.trim(); final description = _groupDescriptionController.text.trim(); if (name.isEmpty) { _showErrorSnackBar('Group name cannot be empty'); return; } if (!mounted) return; setState(() => _isLoading = true); try { final userId = _groupManagementService.currentUserId; if (userId == null) { throw Exception('User not authenticated'); } // Periksa kembali apakah sudah ada grup default bool hasDefaultGroup = _groups.any((group) => group.isDefault); // Jika sudah ada grup default, maka grup baru tidak boleh menjadi default if (hasDefaultGroup) { _isDefaultGroup = false; } // Gunakan RPC untuk membuat grup secara atomik final createdGroup = await _groupManagementService.createGroupViaRPC( name: name, description: description, isPublic: _isPublicGroup, isDefault: _isDefaultGroup, ); if (!mounted) return; if (createdGroup != null) { _showSuccessSnackBar('Group created successfully'); // Jika grup adalah default, sinkronkan semua pengguna if (_isDefaultGroup) { try { final supabase = Supabase.instance.client; final result = await supabase.rpc('sync_users_to_default_groups'); if (!mounted) return; // Log hasil int added = 0, already = 0, reactivated = 0; if (result != null && result is List) { for (final item in result) { if (item['status'] == 'added_new') added++; if (item['status'] == 'already_member') already++; if (item['status'] == 'reactivated') reactivated++; } } print('[DEBUG] Admin: Synchronized all users to default groups:'); print(' - Added: $added users'); print(' - Already members: $already users'); print(' - Reactivated: $reactivated users'); // Jika ada yang berhasil ditambahkan, tampilkan pesan if (added > 0 || reactivated > 0) { _showSuccessSnackBar( 'Added ${added + reactivated} users to the default group', ); } } catch (e) { print( '[WARNING] Admin: Failed to sync users to default groups: $e', ); } } // Refresh groups list dan user list _loadGroups(); _loadUsers(); } else { _showErrorSnackBar('Failed to create group'); setState(() => _isLoading = false); } } catch (e) { print('[ERROR] Failed to create group: $e'); if (mounted) { _showErrorSnackBar( 'Error: ${e.toString().split('Exception:').last.trim()}', ); setState(() => _isLoading = false); } } } Future _showDeleteGroupConfirmation(Group group) async { final shouldDelete = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Delete Group'), content: Text( 'Are you sure you want to delete "${group.name}"? ' 'This action cannot be undone and will delete all messages in this group.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel'), ), ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('Delete'), ), ], ), ); if (shouldDelete == true) { await _deleteGroup(group); } } Future _deleteGroup(Group group) async { if (!mounted) return; setState(() => _isLoading = true); try { print( '[DEBUG] Admin: Attempting to delete group: ${group.id} (${group.name})', ); final success = await _groupManagementService.deleteGroup(group.id); if (!mounted) return; if (success) { print('[DEBUG] Admin: Group deleted successfully'); _showSuccessSnackBar('Group deleted successfully'); _loadGroups(); // Refresh groups list } else { print('[DEBUG] Admin: Group deletion failed - Service returned false'); _showErrorSnackBar( 'Failed to delete group. It may be a default group which cannot be deleted.', ); setState(() => _isLoading = false); } } catch (e) { print('[ERROR] Admin: Failed to delete group: $e'); if (mounted) { _showErrorSnackBar( 'Error: ${e.toString().split('Exception:').last.trim()}', ); setState(() => _isLoading = false); } } } void _showSuccessSnackBar(String message) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message), backgroundColor: Colors.green), ); } void _showErrorSnackBar(String message) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message), backgroundColor: Colors.red), ); } void _showEditGroupDialog(Group group) { // Set nilai awal _groupNameController.text = group.name; _groupDescriptionController.text = group.description; _isPublicGroup = group.isPublic; _isDefaultGroup = group.isDefault; // Cek apakah ada grup default lain selain grup ini bool hasOtherDefaultGroup = _groups.any( (g) => g.isDefault && g.id != group.id, ); showDialog( context: context, builder: (context) => StatefulBuilder( builder: (BuildContext context, StateSetter setDialogState) { return AlertDialog( title: const Text('Edit Group'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: _groupNameController, decoration: const InputDecoration( labelText: 'Group Name', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: _groupDescriptionController, decoration: const InputDecoration( labelText: 'Description', border: OutlineInputBorder(), ), maxLines: 3, ), const SizedBox(height: 16), Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), color: group.isDefault ? Colors.green.shade50 : null, ), padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Default Group', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: group.isDefault ? Colors.green.shade700 : null, ), ), Text( 'All new users join automatically', style: TextStyle( color: group.isDefault ? Colors.green.shade700 : Colors.grey[600], fontSize: 12, ), ), ], ), ), if (group.isDefault) // Jika grup ini adalah default, tampilkan switch yang aktif dan tidak bisa diubah Tooltip( message: 'This is a default group and cannot be changed', child: Switch( value: true, activeColor: Colors.green, onChanged: null, // Tidak bisa diubah ), ) else if (hasOtherDefaultGroup) // Jika ada grup default lain, switch tidak aktif dan tidak bisa diubah Tooltip( message: 'Default group already exists', child: Switch( value: false, activeColor: Colors.grey, onChanged: null, // Tidak bisa diubah ), ) else // Jika tidak ada grup default lain, bisa mengubah grup ini menjadi default Switch( value: _isDefaultGroup, activeColor: Colors.green, onChanged: (value) { setDialogState(() { _isDefaultGroup = value; }); }, ), ], ), ), if (hasOtherDefaultGroup && !group.isDefault) Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( children: [ Icon( Icons.info_outline, color: Colors.orange, size: 16, ), SizedBox(width: 8), Expanded( child: Text( 'Default group already exists', style: TextStyle( color: Colors.grey[600], fontStyle: FontStyle.italic, fontSize: 12, ), ), ), ], ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), ElevatedButton( onPressed: () { Navigator.pop(context); // Jika grup ini adalah default, tetap default // Jika ada grup default lain, grup ini tidak bisa menjadi default if (hasOtherDefaultGroup && !group.isDefault) { _isDefaultGroup = false; } _updateGroup(group); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, ), child: const Text('Update'), ), ], ); }, ), ); } Future _updateGroup(Group group) async { final name = _groupNameController.text.trim(); final description = _groupDescriptionController.text.trim(); if (name.isEmpty) { _showErrorSnackBar('Group name cannot be empty'); return; } if (!mounted) return; setState(() => _isLoading = true); try { // Periksa kembali apakah ada grup default lain bool hasOtherDefaultGroup = _groups.any( (g) => g.isDefault && g.id != group.id, ); // Jika grup ini bukan default dan ada grup default lain, maka tidak bisa menjadi default if (hasOtherDefaultGroup && !group.isDefault) { _isDefaultGroup = false; } // Jika grup ini adalah default, tetap pertahankan sebagai default if (group.isDefault) { _isDefaultGroup = true; } // Buat objek grup yang diperbarui final updatedGroup = Group( id: group.id, name: name, description: description, createdBy: group.createdBy, isPublic: _isPublicGroup, // Selalu true sesuai permintaan isDefault: _isDefaultGroup, iconUrl: group.iconUrl, createdAt: group.createdAt, memberCount: group.memberCount, // Tambahkan ini untuk mencegah null ); // Panggil service untuk memperbarui grup final success = await _groupManagementService.updateGroup(updatedGroup); if (!mounted) return; if (success) { _showSuccessSnackBar('Group updated successfully'); _loadGroups(); // Refresh groups list } else { _showErrorSnackBar('Failed to update group'); setState(() => _isLoading = false); } } catch (e) { print('[ERROR] Failed to update group: $e'); if (mounted) { _showErrorSnackBar( 'Error: ${e.toString().split('Exception:').last.trim()}', ); setState(() => _isLoading = false); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, title: const Text( 'Community Management', style: TextStyle(color: AppColors.primary), ), bottom: TabBar( controller: _tabController, labelColor: AppColors.primary, unselectedLabelColor: Colors.grey, indicatorColor: AppColors.primary, tabs: const [Tab(text: 'Groups'), Tab(text: 'Users')], ), ), body: TabBarView( controller: _tabController, children: [ // Groups tab _buildGroupsTab(), // Users tab _buildUsersTab(), ], ), floatingActionButton: FloatingActionButton( onPressed: _showCreateGroupDialog, backgroundColor: AppColors.primary, child: const Icon(Icons.add), ), ); } Widget _buildGroupsTab() { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_groups.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.group, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'No groups yet', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey[700], ), ), const SizedBox(height: 8), Text( 'Tap the + button to create a new group', style: TextStyle(color: Colors.grey[600]), ), const SizedBox(height: 24), ElevatedButton( onPressed: _createDefaultGroupAndSyncMembers, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, ), child: const Text('Create Default Group'), ), ], ), ); } return RefreshIndicator( onRefresh: _loadGroups, child: Column( children: [ // Groups list Expanded( child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: _groups.length, itemBuilder: (context, index) { final group = _groups[index]; return Card( margin: const EdgeInsets.only(bottom: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 2, child: ListTile( contentPadding: const EdgeInsets.all(16), leading: CircleAvatar( backgroundColor: AppColors.primary.withOpacity(0.2), child: Text( group.name.substring(0, 1).toUpperCase(), style: const TextStyle( color: AppColors.primary, fontWeight: FontWeight.bold, ), ), ), title: Row( children: [ Expanded( child: Text( group.name, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), ), if (group.isDefault) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(12), ), child: const Text( 'Default', style: TextStyle( fontSize: 12, color: Colors.blue, fontWeight: FontWeight.w500, ), ), ), if (!group.isPublic) Container( margin: const EdgeInsets.only(left: 4), padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(12), ), child: const Text( 'Private', style: TextStyle( fontSize: 12, color: Colors.orange, fontWeight: FontWeight.w500, ), ), ), ], ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Text( group.description, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(color: Colors.grey[600]), ), const SizedBox(height: 8), Row( children: [ Icon( Icons.people, size: 16, color: Colors.grey[600], ), const SizedBox(width: 4), Text( '${group.memberCount} members', style: TextStyle(color: Colors.grey[600]), ), ], ), ], ), trailing: IconButton( icon: const Icon(Icons.more_vert), onPressed: () { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(16), ), ), builder: (context) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon( Icons.edit, color: Colors.blue, ), title: const Text('Edit Group'), onTap: () { Navigator.pop(context); _showEditGroupDialog(group); }, ), ListTile( leading: const Icon( Icons.people, color: Colors.green, ), title: const Text('Manage Members'), onTap: () { Navigator.pop(context); _showGroupDetailDialog(group); }, ), if (!group.isDefault) ListTile( leading: const Icon( Icons.delete, color: Colors.red, ), title: const Text('Delete Group'), onTap: () { Navigator.pop(context); _showDeleteGroupConfirmation(group); }, ), ], ), ), ); }, ), onTap: () { _showGroupDetailDialog(group); }, ), ); }, ), ), ], ), ); } void _showGroupDetailDialog(Group group) { showDialog( context: context, builder: (context) => GroupDetailDialog( group: group, onGroupUpdated: () { // Refresh data setelah perubahan grup _loadGroups(); _loadUsers(); }, ), ); } Widget _buildUsersTab() { if (_isLoadingUsers) { return const Center(child: CircularProgressIndicator()); } // Tampilkan tombol refresh jika daftar pengguna kosong if (_users.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.people, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'No users found', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey[700], ), ), const SizedBox(height: 16), ElevatedButton.icon( icon: const Icon(Icons.refresh), label: const Text('Refresh'), onPressed: () async { // Tampilkan loading ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Loading users...'), duration: Duration(seconds: 2), ), ); // Muat ulang data pengguna await _loadUsers(); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, ), ), ], ), ); } return RefreshIndicator( onRefresh: _loadUsers, child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: _users.length, itemBuilder: (context, index) { final user = _users[index]; return Card( margin: const EdgeInsets.only(bottom: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 2, child: ExpansionTile( leading: CircleAvatar( backgroundColor: AppColors.primary.withOpacity(0.2), backgroundImage: user.avatarUrl != null && user.avatarUrl!.isNotEmpty ? NetworkImage(user.avatarUrl!) : null, child: user.avatarUrl == null || user.avatarUrl!.isEmpty ? Text( user.username.isNotEmpty ? user.username.substring(0, 1).toUpperCase() : '?', style: const TextStyle( color: AppColors.primary, fontWeight: FontWeight.bold, ), ) : null, ), title: Text( user.username, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), subtitle: Text( user.groups.isEmpty ? 'No groups joined' : '${user.groups.length} groups: ${user.groups.map((g) => g.groupName).join(", ")}', style: TextStyle(color: Colors.grey[600]), maxLines: 2, overflow: TextOverflow.ellipsis, ), trailing: Icon( user.isActive ? Icons.check_circle : Icons.block, color: user.isActive ? Colors.green : Colors.red, ), children: [ if (user.groups.isEmpty) Padding( padding: const EdgeInsets.all(16), child: Column( children: [ const Text('User is not a member of any group'), ], ), ) else Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Groups:', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(height: 8), ...user.groups.map( (group) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ Icon( Icons.group, size: 16, color: Colors.grey[600], ), const SizedBox(width: 8), Expanded( child: RichText( text: TextSpan( children: [ TextSpan( text: group.groupName, style: const TextStyle( fontSize: 14, color: Colors.black87, ), ), if (group.groupId.isNotEmpty && group.role.isNotEmpty) TextSpan( text: ' (${group.role})', style: TextStyle( fontSize: 12, color: Colors.grey[600], fontStyle: FontStyle.italic, ), ), ], ), ), ), ], ), ), ), ], ), ), ], ), ); }, ), ); } 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}'; } } Future _createDefaultGroupAndSyncMembers() async { if (!mounted) return; setState(() => _isLoading = true); try { // Periksa apakah sudah ada grup default bool hasDefaultGroup = _groups.any((group) => group.isDefault); if (hasDefaultGroup) { _showErrorSnackBar('Default group already exists'); setState(() => _isLoading = false); return; } // Set up a new default group _groupNameController.text = 'General'; _groupDescriptionController.text = 'Default group for all users'; _isPublicGroup = true; _isDefaultGroup = true; // Create the group await _createGroup(); // Refresh data setelah pembuatan grup await _loadGroups(); await _loadUsers(); } catch (e) { print('[ERROR] Failed to create default group: $e'); if (mounted) { _showErrorSnackBar( 'Error: ${e.toString().split('Exception:').last.trim()}', ); setState(() => _isLoading = false); } } } }