/opt/canhelp/apps/mobile/lib/screens/admin
NameSizeModeActions
admin_backup_screen.dart279650644editdlrm
admin_offers_moderation_screen.dart114610644editdlrm
admin_reports_screen.dart110280644editdlrm
admin_screen.dart111740644editdlrm
admin_support_screen.dart244800644editdlrm
admin_tasks_screen.dart174450644editdlrm
admin_users_screen.dart134730644editdlrm
admin_user_detail_screen.dart192530644editdlrm
Edit: /opt/canhelp/apps/mobile/lib/screens/admin/admin_users_screen.dart (13473B)
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import '../../providers/locale_provider.dart'; import '../../services/api_client.dart'; import '../../theme/app_theme.dart'; int _toInt(dynamic v) { if (v == null) return 0; if (v is int) return v; if (v is num) return v.toInt(); return int.tryParse(v.toString()) ?? 0; } class AdminUsersScreen extends StatefulWidget { const AdminUsersScreen({super.key}); @override State createState() => _AdminUsersScreenState(); } class _AdminUsersScreenState extends State { final _searchCtrl = TextEditingController(); final _scrollCtrl = ScrollController(); List> _users = []; int _total = 0; int _page = 1; bool _isLoading = false; bool _hasMore = true; String? _selectedRole; static const _roles = ['', 'customer', 'specialist', 'admin']; @override void initState() { super.initState(); _load(reset: true); _scrollCtrl.addListener(_onScroll); } @override void dispose() { _searchCtrl.dispose(); _scrollCtrl.dispose(); super.dispose(); } void _onScroll() { if (_scrollCtrl.position.pixels >= _scrollCtrl.position.maxScrollExtent - 200) { if (!_isLoading && _hasMore) _load(); } } Future _load({bool reset = false}) async { if (_isLoading) return; if (reset) { _page = 1; _hasMore = true; } if (!_hasMore && !reset) return; setState(() => _isLoading = true); try { final result = await ApiClient().getAdminUsers( page: _page, q: _searchCtrl.text.trim(), role: _selectedRole?.isNotEmpty == true ? _selectedRole : null, ); final rows = (result['data'] as List) .map((e) => Map.from(e as Map)) .toList(); final total = _toInt(result['total']); final limit = _toInt(result['limit']) > 0 ? _toInt(result['limit']) : 20; setState(() { if (reset) { _users = rows; } else { _users.addAll(rows); } _total = total; _hasMore = _users.length < total && rows.length >= limit; if (!reset) _page++; }); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString()), backgroundColor: AppTheme.danger, ), ); } } finally { if (mounted) setState(() => _isLoading = false); } } @override Widget build(BuildContext context) { final locale = context.watch(); final t = locale.t; return Scaffold( backgroundColor: AppTheme.background, appBar: AppBar( backgroundColor: AppTheme.background, elevation: 0, scrolledUnderElevation: 0, foregroundColor: AppTheme.textPrimary, leading: IconButton( icon: const Icon( Icons.arrow_back_ios_new_rounded, size: 20, color: AppTheme.textPrimary, ), onPressed: () => context.canPop() ? context.pop() : context.go('/admin'), ), title: Text( t('admin.users', 'Users'), style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 18, color: AppTheme.textPrimary, ), ), bottom: PreferredSize( preferredSize: const Size.fromHeight(110), child: Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 10), child: Column( children: [ TextField( controller: _searchCtrl, decoration: InputDecoration( hintText: t('admin.search', 'Search...'), prefixIcon: const Icon( Icons.search_rounded, size: 18, color: AppTheme.textHint, ), suffixIcon: _searchCtrl.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear_rounded, size: 16), onPressed: () { _searchCtrl.clear(); _load(reset: true); }, ) : null, contentPadding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10, ), filled: true, fillColor: AppTheme.surface, border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.inputRadius), borderSide: const BorderSide(color: AppTheme.border), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.inputRadius), borderSide: const BorderSide(color: AppTheme.border), ), ), onSubmitted: (_) => _load(reset: true), textInputAction: TextInputAction.search, ), const SizedBox(height: 8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: _roles.map((role) { final label = role.isEmpty ? t('admin.allRoles', 'All') : role == 'customer' ? t('auth.roleCustomer', 'Customer') : role == 'specialist' ? t('auth.roleSpecialist', 'Specialist') : t('auth.roleAdmin', 'Admin'); final selected = _selectedRole == role || (role.isEmpty && (_selectedRole == null || _selectedRole!.isEmpty)); return Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( label: Text(label), selected: selected, onSelected: (_) { setState(() => _selectedRole = role); _load(reset: true); }, selectedColor: AppTheme.primary, labelStyle: TextStyle( color: selected ? Colors.white : AppTheme.textSecondary, fontWeight: FontWeight.w500, fontSize: 12, ), backgroundColor: AppTheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( AppTheme.chipRadius, ), side: BorderSide( color: selected ? AppTheme.primary : AppTheme.border, ), ), padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), ), ); }).toList(), ), ), ], ), ), ), ), body: RefreshIndicator( color: AppTheme.primary, onRefresh: () => _load(reset: true), child: _users.isEmpty && !_isLoading ? Center( child: Text( t('admin.noData', 'No data'), style: const TextStyle(color: AppTheme.textSecondary), ), ) : ListView.separated( controller: _scrollCtrl, padding: const EdgeInsets.fromLTRB(12, 8, 12, 24), itemCount: _users.length + (_hasMore ? 1 : 0), separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, i) { if (i == _users.length) { return const Center( child: Padding( padding: EdgeInsets.all(16), child: CircularProgressIndicator( color: AppTheme.primary, ), ), ); } final user = _users[i]; return _UserTile( user: user, locale: locale, onTap: () => context.push('/admin/users/${user['id']}'), ); }, ), ), ); } } class _UserTile extends StatelessWidget { const _UserTile({ required this.user, required this.locale, required this.onTap, }); final Map user; final LocaleProvider locale; final VoidCallback onTap; @override Widget build(BuildContext context) { final role = user['role'] as String? ?? 'customer'; final isActive = user['isActive'] as bool? ?? true; final firstName = user['firstName'] as String? ?? ''; final lastName = user['lastName'] as String? ?? ''; final name = (user['name'] as String?)?.isNotEmpty == true ? user['name'] as String : '$firstName $lastName'.trim(); final email = user['email'] as String? ?? ''; final avatar = user['image'] as String? ?? user['avatar'] as String?; final roleColor = switch (role) { 'admin' => AppTheme.danger, 'specialist' => AppTheme.emerald, _ => AppTheme.primary, }; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(AppTheme.cardRadius), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.cardRadius), border: Border.all(color: AppTheme.border), ), child: Row( children: [ CircleAvatar( radius: 22, backgroundColor: AppTheme.primary.withOpacity(0.10), backgroundImage: avatar != null && avatar.isNotEmpty ? NetworkImage(avatar) : null, child: avatar == null || avatar.isEmpty ? Text( name.isNotEmpty ? name[0].toUpperCase() : '?', style: const TextStyle( color: AppTheme.primary, fontWeight: FontWeight.w700, ), ) : null, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name.isNotEmpty ? name : email, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( email, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Row( children: [ _Chip(role, roleColor), const SizedBox(width: 6), _Chip( isActive ? locale.t('admin.active', 'Active') : locale.t('admin.inactive', 'Inactive'), isActive ? AppTheme.emerald : AppTheme.textSecondary, ), ], ), ], ), ), const Icon( Icons.chevron_right_rounded, size: 18, color: AppTheme.textHint, ), ], ), ), ); } } class _Chip extends StatelessWidget { const _Chip(this.label, this.color); final String label; final Color color; @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration( color: color.withOpacity(0.10), borderRadius: BorderRadius.circular(AppTheme.chipRadius), ), child: Text( label, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: color), ), ); }