/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_tasks_screen.dart (17445B)
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'; double? _toDouble(dynamic v) { if (v == null) return null; if (v is num) return v.toDouble(); return double.tryParse(v.toString()); } 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 AdminTasksScreen extends StatefulWidget { const AdminTasksScreen({super.key}); @override State createState() => _AdminTasksScreenState(); } class _AdminTasksScreenState extends State { final _searchCtrl = TextEditingController(); final _scrollCtrl = ScrollController(); List> _tasks = []; int _total = 0; int _page = 1; bool _isLoading = false; bool _hasMore = true; String? _selectedStatus; static const _statuses = [ '', 'open', 'in_progress', 'completed', 'cancelled', 'draft', ]; @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().getAdminTasks( page: _page, q: _searchCtrl.text.trim(), status: _selectedStatus?.isNotEmpty == true ? _selectedStatus : null, ); final rows = (result['data'] as List).map((e) { final item = e as Map; // API returns { task: {...}, customer: {...} } if (item.containsKey('task')) { return Map.from(item['task'] as Map) ..['_customer'] = item['customer']; } return Map.from(item); }).toList(); final total = _toInt(result['total']); final limit = _toInt(result['limit']) > 0 ? _toInt(result['limit']) : 20; setState(() { if (reset) { _tasks = rows; } else { _tasks.addAll(rows); } _total = total; _hasMore = _tasks.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); } } Future _cancelTask(String id) async { try { await ApiClient().adminCancelTask(id); setState(() { final idx = _tasks.indexWhere((t) => t['id'] == id); if (idx != -1) _tasks[idx] = {..._tasks[idx], 'status': 'cancelled'}; }); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString()), backgroundColor: AppTheme.danger, ), ); } } } Future _deleteTask(String id, LocaleProvider locale) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(locale.t('admin.deleteConfirm', 'Delete this task?')), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: Text(locale.t('common.cancel', 'Cancel')), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), style: FilledButton.styleFrom(backgroundColor: AppTheme.danger), child: Text(locale.t('common.delete', 'Delete')), ), ], ), ); if (confirmed != true) return; try { await ApiClient().adminDeleteTask(id); setState(() => _tasks.removeWhere((t) => t['id'] == id)); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString()), backgroundColor: AppTheme.danger, ), ); } } } @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.tasks', 'Tasks'), 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: _statuses.map((s) { final label = s.isEmpty ? t('admin.allStatuses', 'All') : _statusLabel(s, t); final selected = _selectedStatus == s || (s.isEmpty && (_selectedStatus == null || _selectedStatus!.isEmpty)); return Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( label: Text(label), selected: selected, onSelected: (_) { setState(() => _selectedStatus = s); _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: _tasks.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: _tasks.length + (_hasMore ? 1 : 0), separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, i) { if (i == _tasks.length) { return const Center( child: Padding( padding: EdgeInsets.all(16), child: CircularProgressIndicator( color: AppTheme.primary, ), ), ); } final task = _tasks[i]; return _TaskTile( task: task, locale: locale, onCancel: () => _cancelTask(task['id'] as String), onDelete: () => _deleteTask(task['id'] as String, locale), ); }, ), ), ); } String _statusLabel(String status, String Function(String, [String?]) t) => switch (status) { 'open' => t('tasks.statusOpen', 'Open'), 'in_progress' => t('tasks.statusInProgress', 'In progress'), 'completed' => t('tasks.statusCompleted', 'Completed'), 'cancelled' => t('tasks.statusCancelled', 'Cancelled'), 'draft' => t('tasks.statusDraft', 'Draft'), _ => status, }; } class _TaskTile extends StatelessWidget { const _TaskTile({ required this.task, required this.locale, required this.onCancel, required this.onDelete, }); final Map task; final LocaleProvider locale; final VoidCallback onCancel; final VoidCallback onDelete; @override Widget build(BuildContext context) { final t = locale.t; final title = task['title'] as String? ?? ''; final status = task['status'] as String? ?? ''; final budget = _toDouble(task['budget']); final customer = task['_customer'] as Map?; final customerName = customer?['name'] as String? ?? customer?['email'] as String? ?? ''; final createdAt = task['createdAt'] as String?; final statusColor = switch (status) { 'open' => AppTheme.primary, 'in_progress' => AppTheme.amber, 'completed' => AppTheme.emerald, 'cancelled' => AppTheme.danger, _ => AppTheme.textSecondary, }; final statusLabel = switch (status) { 'open' => t('tasks.statusOpen', 'Open'), 'in_progress' => t('tasks.statusInProgress', 'In progress'), 'completed' => t('tasks.statusCompleted', 'Completed'), 'cancelled' => t('tasks.statusCancelled', 'Cancelled'), 'draft' => t('tasks.statusDraft', 'Draft'), _ => status, }; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.surface, borderRadius: BorderRadius.circular(AppTheme.cardRadius), border: Border.all(color: AppTheme.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( title, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: statusColor.withOpacity(0.10), borderRadius: BorderRadius.circular(AppTheme.chipRadius), ), child: Text( statusLabel, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: statusColor, ), ), ), ], ), if (customerName.isNotEmpty) ...[ const SizedBox(height: 4), Text( customerName, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ], if (budget != null || createdAt != null) ...[ const SizedBox(height: 4), Row( children: [ if (budget != null) Text( '€${budget.toStringAsFixed(2)}', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppTheme.emerald, ), ), const Spacer(), if (createdAt != null) Text( createdAt.length >= 10 ? createdAt.substring(0, 10) : createdAt, style: const TextStyle( fontSize: 11, color: AppTheme.textHint, ), ), ], ), ], const SizedBox(height: 10), const Divider(height: 1, color: AppTheme.border), const SizedBox(height: 8), Row( children: [ if (status != 'cancelled') ...[ _ActionBtn( label: t('admin.cancel', 'Cancel'), color: AppTheme.warning, icon: Icons.cancel_outlined, onTap: onCancel, ), const SizedBox(width: 8), ], _ActionBtn( label: t('common.delete', 'Delete'), color: AppTheme.danger, icon: Icons.delete_outline_rounded, onTap: onDelete, ), ], ), ], ), ); } } class _ActionBtn extends StatelessWidget { const _ActionBtn({ required this.label, required this.color, required this.icon, required this.onTap, }); final String label; final Color color; final IconData icon; final VoidCallback onTap; @override Widget build(BuildContext context) => GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(AppTheme.smallRadius), border: Border.all(color: color.withOpacity(0.25)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 13, color: color), const SizedBox(width: 4), Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: color, ), ), ], ), ), ); }