MIF_E31222656/lib/screens/admin/user_management.dart

1466 lines
57 KiB
Dart

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<UserManagement> createState() => _UserManagementState();
}
class _UserManagementState extends State<UserManagement> {
final _supabase = Supabase.instance.client;
bool _isLoading = true;
List<Map<String, dynamic>> _users = [];
List<Map<String, dynamic>> _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<void> _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<Map>
final users = List<Map<String, dynamic>>.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<Map>
final profiles = List<Map<String, dynamic>>.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<Map<String, dynamic>>.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<Map<String, dynamic>>.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<void> _assignAdminRole(String userId) async {
// Show confirmation dialog first
final shouldProceed = await showDialog<bool>(
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<void> _removeAdminRole(String userId) async {
// Show confirmation dialog first
final shouldProceed = await showDialog<bool>(
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<void> _viewUserDetails(Map<String, dynamic> 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<String, dynamic> 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<void> _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<void> _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<void> _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<bool> _confirmDismiss(String userId, String email) async {
// Show a confirmation dialog
final bool? result = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Confirm Delete'),
content: Text('Are you sure you want to delete user $email?'),
actions: <Widget>[
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<bool>(
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<bool> _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,
),
],
),
),
],
),
),
);
}
}