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/admin/admin_dashboard.dart'; class UserManagement extends StatefulWidget { const UserManagement({super.key}); @override State createState() => _UserManagementState(); } class _UserManagementState extends State { final _supabase = Supabase.instance.client; bool _isLoading = true; List> _users = []; List> _filteredUsers = []; String _searchQuery = ''; // Modern Color Scheme static const Color primaryGreen = Color(0xFF0F6848); static const Color lightGreen = Color(0xFF4CAF50); static const Color surfaceGreen = Color(0xFFF1F8E9); static const Color cardWhite = Colors.white; static const Color textPrimary = Color(0xFF1B5E20); static const Color textSecondary = Color(0xFF757575); @override void initState() { super.initState(); _loadUsers(); } Future _loadUsers() async { setState(() => _isLoading = true); try { debugPrint( 'Starting to load all users from database using get_all_users function', ); // Try to execute the get_all_users function final response = await _supabase.rpc('get_all_users'); debugPrint('get_all_users response type: ${response.runtimeType}'); debugPrint('get_all_users response length: ${response.length}'); // Convert to List final users = List>.from(response); // Debug each user for (var user in users) { debugPrint( 'User found: ${user['email']} with ID: ${user['user_id']}, role: ${user['role']}', ); } if (mounted) { setState(() { _users = users; _filteredUsers = _users; _isLoading = false; // Debug the final list debugPrint('Total users loaded into UI: ${_users.length}'); }); } } catch (e) { debugPrint('Error loading users with get_all_users: $e'); // Fallback to manual query if the RPC fails try { debugPrint('Falling back to manual join query'); // Directly fetch all users from profiles table final profilesResponse = await _supabase .from('profiles') .select('*') .order('created_at', ascending: false); debugPrint('Profiles loaded: ${profilesResponse.length}'); // Convert to List final profiles = List>.from(profilesResponse); // Debug each profile for (var profile in profiles) { debugPrint( 'Profile found: ${profile['email']} with ID: ${profile['user_id']}', ); } // Fetch all user roles final rolesResponse = await _supabase.from('user_roles').select('*'); final roles = List>.from(rolesResponse); debugPrint('Roles loaded: ${roles.length}'); // Join profiles with roles for (var profile in profiles) { final userId = profile['user_id']; // Find matching role final userRole = roles.firstWhere( (r) => r['user_id'] == userId, orElse: () => {'role': null}, ); // Add role to profile profile['role'] = userRole['role']; debugPrint('User ${profile['email']} has role: ${profile['role']}'); } if (mounted) { setState(() { _users = profiles; _filteredUsers = _users; _isLoading = false; // Debug the final list debugPrint('Total users loaded into UI: ${_users.length}'); }); } } catch (fallbackError) { debugPrint('Even fallback query failed: $fallbackError'); // Last resort: try a direct query to auth.users through RPC try { debugPrint('Trying last resort query to auth.users'); // Create a simple function to get all auth users if it doesn't exist final authUsersResponse = await _supabase.rpc('get_all_auth_users'); final authUsers = List>.from(authUsersResponse); debugPrint('Auth users query returned ${authUsers.length} users'); if (mounted) { setState(() { _users = authUsers; _filteredUsers = _users; _isLoading = false; }); } } catch (lastError) { debugPrint('Last resort query failed: $lastError'); if (mounted) { setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: Unable to load users. Please try again.'), backgroundColor: Colors.red.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } } } } } void _filterUsers(String query) { setState(() { _searchQuery = query; if (query.isEmpty) { _filteredUsers = _users; } else { _filteredUsers = _users.where((user) { final email = user['email']?.toString().toLowerCase() ?? ''; final username = user['username']?.toString().toLowerCase() ?? user['farm_name']?.toString().toLowerCase() ?? ''; final searchLower = query.toLowerCase(); return email.contains(searchLower) || username.contains(searchLower); }).toList(); } }); } Future _assignAdminRole(String userId) async { // Show confirmation dialog first final shouldProceed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Confirm Admin Promotion'), content: const Text( 'Are you sure you want to promote this user to admin? ' 'This will grant them full access to the admin panel.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: const Color.fromARGB(255, 255, 255, 255), ), child: const Text('Promote to Admin'), ), ], ), ); // If user canceled, don't proceed if (shouldProceed != true) return; try { // Cek jumlah admin saat ini untuk keamanan final adminCountResponse = await _supabase .from('user_roles') .select('*') .eq('role', 'admin') .count(); final adminCount = adminCountResponse.count ?? 0; // Cek apakah pengguna sudah memiliki role final existingRole = await _supabase .from('user_roles') .select() .eq('user_id', userId) .maybeSingle(); if (existingRole != null) { // Update role jika sudah ada await _supabase .from('user_roles') .update({'role': 'admin'}) .eq('user_id', userId); } else { // Tambahkan role baru jika belum ada await _supabase.from('user_roles').insert({ 'user_id': userId, 'role': 'admin', }); } // Refresh data await _loadUsers(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('User successfully promoted to admin'), backgroundColor: Colors.green.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } } catch (e) { debugPrint('Error assigning admin role: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } } } Future _removeAdminRole(String userId) async { // Show confirmation dialog first final shouldProceed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Confirm Admin Removal'), content: const Text( 'Are you sure you want to remove admin privileges from this user? ' 'They will no longer have access to the admin panel.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: const Color.fromARGB(255, 255, 255, 255), ), child: const Text('Remove Admin'), ), ], ), ); // If user canceled, don't proceed if (shouldProceed != true) return; try { // Cek jumlah admin saat ini untuk keamanan final adminCountResponse = await _supabase .from('user_roles') .select('*') .eq('role', 'admin') .count(); final adminCount = adminCountResponse.count ?? 0; // Pastikan selalu ada minimal 1 admin if (adminCount <= 1) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Cannot remove the last admin'), backgroundColor: Colors.red.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } return; } // Hapus role admin await _supabase .from('user_roles') .delete() .eq('user_id', userId) .eq('role', 'admin'); // Refresh data await _loadUsers(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Admin privileges removed'), backgroundColor: Colors.green.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } } catch (e) { debugPrint('Error removing admin role: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } } } Future _viewUserDetails(Map user) async { // Show user details in a modal bottom sheet await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => _buildUserDetailsSheet(user), ); } Widget _buildUserDetailsSheet(Map user) { final isAdmin = user['role'] == 'admin'; final email = user['email'] ?? 'No Email'; final username = user['username'] ?? user['farm_name'] ?? 'No Username'; final fullName = user['full_name'] ?? 'No Name'; final phone = user['phone'] ?? 'No Phone'; final address = user['address'] ?? 'No Address'; final createdAt = user['created_at'] != null ? DateTime.parse(user['created_at']) : null; final formattedDate = createdAt != null ? '${createdAt.day}/${createdAt.month}/${createdAt.year}' : 'Unknown'; return Container( padding: const EdgeInsets.all(20), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'User Details', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: textPrimary, ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), ), ], ), const Divider(), const SizedBox(height: 12), // User avatar and status Row( children: [ CircleAvatar( radius: 30, backgroundColor: isAdmin ? Colors.blue.shade100 : surfaceGreen, child: user['avatar_url'] != null && user['avatar_url'].toString().isNotEmpty ? ClipOval( child: Image.network( user['avatar_url'], width: 60, height: 60, fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, color: isAdmin ? Colors.blue : primaryGreen, value: loadingProgress.expectedTotalBytes != null ? loadingProgress .cumulativeBytesLoaded / loadingProgress .expectedTotalBytes! : null, ), ), ); }, errorBuilder: (context, error, stackTrace) => Icon( Icons.person, size: 30, color: isAdmin ? Colors.blue : primaryGreen, ), ), ) : Icon( Icons.person, size: 30, color: isAdmin ? Colors.blue : primaryGreen, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( username, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), Text( email, style: TextStyle(fontSize: 12, color: textSecondary), ), const SizedBox(height: 4), Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: isAdmin ? Colors.blue.shade100 : surfaceGreen, borderRadius: BorderRadius.circular(12), ), child: Text( isAdmin ? 'Admin' : 'User', style: TextStyle( fontSize: 12, color: isAdmin ? Colors.blue : primaryGreen, fontWeight: FontWeight.bold, ), ), ), const SizedBox(width: 8), Text( 'Joined: $formattedDate', style: TextStyle(fontSize: 12, color: textSecondary), ), ], ), ], ), ), ], ), const SizedBox(height: 24), // User information _buildDetailItem('Full Name', fullName), _buildDetailItem('Phone', phone), _buildDetailItem('Address', address), const SizedBox(height: 24), // Action buttons Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildActionButton( isAdmin ? 'Remove Admin' : 'Make Admin', isAdmin ? Icons.person_remove : Icons.admin_panel_settings, isAdmin ? Colors.red : Colors.blue, () { Navigator.pop(context); if (isAdmin) { _removeAdminRole(user['user_id']); } else { _assignAdminRole(user['user_id']); } }, subtitle: 'Requires confirmation', ), _buildActionButton( 'Reset Password', Icons.lock_reset, Colors.orange, () { Navigator.pop(context); _showResetPasswordConfirmation(user['email']); }, subtitle: 'Sends email link', ), ], ), const SizedBox(height: 20), // Delete user button (separated for emphasis) Center( child: _buildActionButton( 'Delete User', Icons.delete_forever, Colors.red, () { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text( 'Swipe the user card from right to left to delete', ), backgroundColor: Colors.orange.shade400, behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 3), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); }, subtitle: 'Swipe card to delete', ), ), ], ), ); } Widget _buildDetailItem(String label, String value) { return Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: 12, color: textSecondary)), const SizedBox(height: 4), Text(value, style: const TextStyle(fontSize: 16)), ], ), ); } Widget _buildActionButton( String label, IconData icon, Color color, VoidCallback onTap, { String? subtitle, }) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.3)), ), child: Column( children: [ Icon(icon, color: color), const SizedBox(height: 8), Text( label, style: TextStyle(color: color, fontWeight: FontWeight.w500), ), if (subtitle != null) Text( subtitle, style: TextStyle(color: Colors.grey[600], fontSize: 12), ), ], ), ), ); } Future _showResetPasswordConfirmation(String email) async { return showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Reset Password'), content: Text('Send password reset link to $email?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), ElevatedButton( onPressed: () { Navigator.pop(context); _sendPasswordResetEmail(email); }, style: ElevatedButton.styleFrom(backgroundColor: primaryGreen), child: const Text('Send Reset Link'), ), ], ), ); } Future _sendPasswordResetEmail(String email) async { try { await _supabase.auth.resetPasswordForEmail(email); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Password reset link sent to $email'), backgroundColor: Colors.green.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } } catch (e) { debugPrint('Error sending password reset: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } } } Future _deleteUser(String userId, String email) async { try { // Check if user is an admin final userRole = await _supabase .from('user_roles') .select() .eq('user_id', userId) .eq('role', 'admin') .maybeSingle(); // If user is an admin, check if they are the last admin if (userRole != null) { final adminCountResponse = await _supabase .from('user_roles') .select('*') .eq('role', 'admin') .count(); final adminCount = adminCountResponse.count ?? 0; // Ensure there's always at least one admin if (adminCount <= 1) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Cannot delete the last admin user'), backgroundColor: Colors.red.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } return; } } // Delete the user - this requires admin privileges in Supabase await _supabase.rpc('delete_user', params: {'user_id_param': userId}); // Refresh data await _loadUsers(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('User $email has been deleted'), backgroundColor: Colors.green.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } } catch (e) { debugPrint('Error deleting user: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } } } Future _confirmDismiss(String userId, String email) async { // Show a confirmation dialog final bool? result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Confirm Delete'), content: Text('Are you sure you want to delete user $email?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: const Color.fromARGB(255, 255, 255, 255), ), child: const Text('Delete', style: TextStyle(color: Colors.red)), ), ], ); }, ); // If the user confirmed deletion, proceed to the full delete confirmation if (result == true) { // Text controller for the confirmation text field final TextEditingController confirmController = TextEditingController(); bool canDelete = false; // Show second confirmation dialog requiring DELETE text final shouldProceed = await showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setState) { return AlertDialog( contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), titlePadding: const EdgeInsets.fromLTRB(24, 16, 24, 0), title: const Text('Final Confirmation'), content: Container( width: double.maxFinite, constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'WARNING: This action cannot be undone.', style: TextStyle( color: Colors.red, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text('Delete user: $email?'), const SizedBox(height: 8), const Text('Type "DELETE" to confirm:'), Container( height: 40, margin: const EdgeInsets.only(top: 8), child: TextField( controller: confirmController, autofocus: true, onChanged: (value) { setState(() { canDelete = value == 'DELETE'; }); }, decoration: const InputDecoration( hintText: 'Type DELETE in all caps', contentPadding: EdgeInsets.symmetric( horizontal: 12, vertical: 4, ), isDense: true, border: OutlineInputBorder(), ), ), ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel'), ), ElevatedButton( onPressed: canDelete ? () => Navigator.of(context).pop(true) : null, style: ElevatedButton.styleFrom( foregroundColor: Colors.red, backgroundColor: Colors.white, disabledBackgroundColor: Colors.grey.shade300, ), child: const Text('Delete User'), ), ], ); }, ), ); return shouldProceed == true; } return false; } // Menangani tombol kembali Future _onWillPop() async { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (context) => const AdminDashboard()), ); return false; } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: _onWillPop, child: Scaffold( backgroundColor: Colors.grey[50], appBar: AppBar( backgroundColor: primaryGreen, surfaceTintColor: Colors.transparent, elevation: 0, title: const Text( 'User Management', style: TextStyle(fontWeight: FontWeight.w600, color: Colors.white), ), leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), automaticallyImplyLeading: false, actions: [ IconButton( icon: const Icon(Icons.help_outline, color: Colors.white), onPressed: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text( 'Swipe users left to delete, pull down to refresh', ), backgroundColor: Colors.black87, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); }, tooltip: 'Help', ), ], ), body: Column( children: [ // Stats and search container Container( padding: const EdgeInsets.only( left: 16, right: 16, bottom: 16, top: 8, ), decoration: BoxDecoration( color: primaryGreen, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24), ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: Column( children: [ // User stats Row( children: [ _buildStatCard( 'Total Users', '${_users.length}', Icons.people_alt_outlined, ), const SizedBox(width: 8), _buildStatCard( 'Admins', '${_users.where((u) => u['role'] == 'admin').length}', Icons.admin_panel_settings_outlined, ), ], ), const SizedBox(height: 16), // Search bar Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, 2), ), ], ), child: TextField( onChanged: _filterUsers, decoration: InputDecoration( hintText: 'Search users...', prefixIcon: const Icon( Icons.search, color: primaryGreen, ), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon( Icons.clear, color: Colors.grey, ), onPressed: () => _filterUsers(''), ) : null, filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric( vertical: 12, ), ), ), ), ], ), ), // Gesture hints Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Row( children: [ const Icon(Icons.swipe_left, size: 16, color: primaryGreen), const SizedBox(width: 4), Text( 'Swipe left to delete', style: TextStyle( fontSize: 12, color: Colors.grey[700], fontWeight: FontWeight.w500, ), ), const Spacer(), const Icon(Icons.refresh, size: 16, color: primaryGreen), const SizedBox(width: 4), Text( 'Pull down to refresh', style: TextStyle( fontSize: 12, color: Colors.grey[700], fontWeight: FontWeight.w500, ), ), ], ), ), // User list Expanded( child: _isLoading ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: primaryGreen), const SizedBox(height: 16), Text( 'Loading users...', style: TextStyle( color: Colors.grey[600], fontWeight: FontWeight.w500, ), ), ], ), ) : _filteredUsers.isEmpty ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.search_off, size: 48, color: Colors.grey[400], ), const SizedBox(height: 16), Text( _searchQuery.isEmpty ? 'No users found' : 'No matching users', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey[600], ), ), if (_searchQuery.isNotEmpty) TextButton( onPressed: () => _filterUsers(''), child: const Text('Clear search'), ), ], ), ) : RefreshIndicator( color: primaryGreen, onRefresh: _loadUsers, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.only(bottom: 16), itemCount: _filteredUsers.length, itemBuilder: (context, index) { final user = _filteredUsers[index]; final isAdmin = user['role'] == 'admin'; final userId = user['user_id']; final email = user['email'] ?? 'No Email'; final username = user['username'] ?? user['farm_name'] ?? 'No Username'; final createdAt = user['created_at'] != null ? DateTime.parse(user['created_at']) : null; final formattedDate = createdAt != null ? '${createdAt.day}/${createdAt.month}/${createdAt.year}' : 'Unknown'; return Dismissible( key: Key(userId), direction: DismissDirection.endToStart, confirmDismiss: (direction) async { return await _confirmDismiss(userId, email); }, background: Container( alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20.0), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(16), ), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.delete_forever, color: Colors.white, size: 28, ), SizedBox(height: 4), Text( 'Delete', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ], ), ), onDismissed: (direction) { _deleteUser(userId, email); }, child: Card( margin: const EdgeInsets.symmetric( horizontal: 16, vertical: 6, ), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide( color: isAdmin ? Colors.blue.withOpacity(0.3) : Colors.grey.withOpacity(0.15), width: 1, ), ), child: InkWell( onTap: () => _viewUserDetails(user), borderRadius: BorderRadius.circular(16), child: Padding( padding: const EdgeInsets.symmetric( vertical: 12, horizontal: 4, ), child: ListTile( minLeadingWidth: 36, horizontalTitleGap: 6, contentPadding: const EdgeInsets.symmetric( horizontal: 8, ), leading: Hero( tag: 'avatar-$userId', child: CircleAvatar( radius: 22, backgroundColor: isAdmin ? Colors.blue.withOpacity(0.2) : surfaceGreen, child: user['avatar_url'] != null && user['avatar_url'] .toString() .isNotEmpty ? ClipOval( child: Image.network( user['avatar_url'], width: 44, height: 44, fit: BoxFit.cover, loadingBuilder: ( context, child, loadingProgress, ) { if (loadingProgress == null) return child; return Center( child: SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, value: loadingProgress .expectedTotalBytes != null ? loadingProgress .cumulativeBytesLoaded / loadingProgress .expectedTotalBytes! : null, ), ), ); }, errorBuilder: ( context, error, stackTrace, ) => Icon( Icons.person, color: isAdmin ? Colors .blue : primaryGreen, size: 24, ), ), ) : Icon( Icons.person, color: isAdmin ? Colors.blue : primaryGreen, size: 24, ), ), ), title: Text( username, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 15, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( 'Email: $email', style: TextStyle( fontSize: 12, color: Colors.grey[700], ), overflow: TextOverflow.ellipsis, maxLines: 1, ), const SizedBox(height: 2), Wrap( spacing: 4, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.calendar_today, size: 10, color: Colors.grey[600], ), const SizedBox(width: 2), Text( 'Joined: $formattedDate', style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), ], ), if (isAdmin) Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 1, ), decoration: BoxDecoration( color: Colors.blue .withOpacity(0.15), borderRadius: BorderRadius.circular( 8, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.verified, size: 9, color: Colors.blue[700], ), const SizedBox(width: 2), Text( 'Admin', style: TextStyle( fontSize: 9, color: Colors.blue[700], fontWeight: FontWeight.bold, ), ), ], ), ), ], ), ], ), isThreeLine: true, trailing: SizedBox( width: 28, height: 28, child: IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints(), icon: Icon( isAdmin ? Icons.person_remove : Icons.admin_panel_settings, color: isAdmin ? Colors.red : primaryGreen, size: 18, ), onPressed: () { if (isAdmin) { _removeAdminRole(userId); } else { _assignAdminRole(userId); } }, tooltip: isAdmin ? 'Remove admin role' : 'Make admin', ), ), ), ), ), ), ); }, ), ), ), ], ), ), ); } Widget _buildStatCard(String title, String value, IconData icon) { return Expanded( child: Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: primaryGreen.withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: primaryGreen, size: 18), ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( value, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: primaryGreen, ), overflow: TextOverflow.ellipsis, ), Text( title, style: TextStyle( fontSize: 11, color: Colors.grey[700], fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ), ); } }