import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/status_badge.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/error_view.dart'; import '../../shared/models/task.dart'; import 'tasks_provider.dart'; class TaskListScreen extends ConsumerStatefulWidget { const TaskListScreen({super.key}); @override ConsumerState createState() => _TaskListScreenState(); } class _TaskListScreenState extends ConsumerState { // Default: show only active tasks (todo + in_progress) final Set _selectedStatuses = {'todo', 'in_progress'}; final _searchCtrl = TextEditingController(); static const _filters = [ ('todo', 'To Do'), ('in_progress', 'In Progress'), ('done', 'Done'), ]; @override void initState() { super.initState(); _searchCtrl.addListener(_reload); // Initial load with default filters applied server-side is not possible for multi, // so we load all and filter client-side by default. WidgetsBinding.instance.addPostFrameCallback((_) => _reload()); } @override void dispose() { _searchCtrl.removeListener(_reload); _searchCtrl.dispose(); super.dispose(); } void _toggle(String status) { setState(() { if (_selectedStatuses.contains(status)) { _selectedStatuses.remove(status); } else { _selectedStatuses.add(status); } }); _reload(); } void _reload() { // Single status → pass to API for server-side filtering. // Multiple or none → fetch all, filter client-side. final singleStatus = _selectedStatuses.length == 1 ? _selectedStatuses.first : null; ref.read(tasksProvider.notifier).load( status: singleStatus ?? 'all', search: _searchCtrl.text, ); } List _applyClientFilter(List items) { if (_selectedStatuses.isEmpty || _selectedStatuses.length == 3) return items; if (_selectedStatuses.length == 1) return items; // already server-filtered return items.where((t) => _selectedStatuses.contains(t.status)).toList(); } @override Widget build(BuildContext context) { final state = ref.watch(tasksProvider); return Scaffold( appBar: AppBar( title: const Text('Tasks'), actions: [ IconButton( icon: const Icon(Icons.notifications_outlined), onPressed: () => context.push('/notifications'), ), IconButton( icon: const Icon(Icons.person_outline), onPressed: () => context.push('/profile'), ), ], ), body: Column( children: [ // Search bar Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: TextField( controller: _searchCtrl, decoration: InputDecoration( hintText: 'Search tasks...', prefixIcon: const Icon(Icons.search, color: AppColors.textSub), suffixIcon: _searchCtrl.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, color: AppColors.textSub), onPressed: _searchCtrl.clear, ) : null, ), ), ), // Multi-select filter chips SizedBox( height: 40, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), children: _filters.map((f) { final (val, label) = f; final active = _selectedStatuses.contains(val); return Padding( padding: const EdgeInsets.only(right: 8), child: FilterChip( label: Text(label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: active ? Colors.white : AppColors.textSub, )), selected: active, selectedColor: _chipColor(val), backgroundColor: AppColors.surface, checkmarkColor: Colors.white, side: BorderSide(color: active ? _chipColor(val) : AppColors.border), onSelected: (_) => _toggle(val), showCheckmark: false, avatar: active ? Icon(_chipIcon(val), size: 14, color: Colors.white) : null, ), ); }).toList(), ), ), const SizedBox(height: 8), // Task list Expanded( child: state.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => ErrorView( message: e.toString(), onRetry: _reload, ), data: (paged) { final items = _applyClientFilter(paged.items); if (items.isEmpty) { return EmptyState( message: _selectedStatuses.isEmpty ? 'No tasks found' : 'No ${_statusLabel()} tasks', icon: Icons.check_box_outline_blank, actionLabel: 'Create Task', onAction: () => context.push('/tasks/create'), ); } return RefreshIndicator( color: AppColors.primary, onRefresh: () async => _reload(), child: ListView.separated( padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), itemCount: items.length, separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (_, i) => _TaskCard( task: items[i], onStatusChanged: _reload, ), ), ); }, ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () async { await context.push('/tasks/create'); if (mounted) _reload(); }, child: const Icon(Icons.add), ), ); } Color _chipColor(String status) => switch (status) { 'in_progress' => AppColors.warning, 'done' => AppColors.primary, _ => AppColors.blue, }; IconData _chipIcon(String status) => switch (status) { 'in_progress' => Icons.timelapse, 'done' => Icons.check_circle_outline, _ => Icons.radio_button_unchecked, }; String _statusLabel() { if (_selectedStatuses.length == 1) { return switch (_selectedStatuses.first) { 'in_progress' => 'in progress', 'done' => 'completed', _ => 'to do', }; } return 'active'; } } // ── Task card with inline status edit ──────────────────────────────────────── class _TaskCard extends ConsumerStatefulWidget { final Task task; final VoidCallback onStatusChanged; const _TaskCard({required this.task, required this.onStatusChanged}); @override ConsumerState<_TaskCard> createState() => _TaskCardState(); } class _TaskCardState extends ConsumerState<_TaskCard> { bool _updatingStatus = false; Future _changeStatus(String newStatus) async { if (newStatus == widget.task.status) return; setState(() => _updatingStatus = true); try { await updateTask(widget.task.id, {'status': newStatus}); widget.onStatusChanged(); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString()), backgroundColor: AppColors.danger), ); } } finally { if (mounted) setState(() => _updatingStatus = false); } } void _showStatusPicker() { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (_) => SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), child: Row(children: [ const Expanded(child: Text('Change Status', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700))), IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ]), ), const Divider(height: 1), ...[ ('todo', 'To Do', Icons.radio_button_unchecked, AppColors.blue), ('in_progress', 'In Progress', Icons.timelapse, AppColors.warning), ('done', 'Done', Icons.check_circle_outline, AppColors.primary), ].map((item) { final (val, label, icon, color) = item; final isCurrent = widget.task.status == val; return ListTile( leading: Icon(icon, color: color), title: Text(label, style: TextStyle( fontWeight: isCurrent ? FontWeight.w700 : FontWeight.w500, color: isCurrent ? color : AppColors.text, )), trailing: isCurrent ? Icon(Icons.check, color: color, size: 18) : null, onTap: () { Navigator.pop(context); _changeStatus(val); }, ); }), ], ), ), ), ); } @override Widget build(BuildContext context) { final t = widget.task; return GestureDetector( onTap: () => context.push('/tasks/${t.id}'), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: t.isOverdue ? AppColors.dangerLight : AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Expanded(child: Text(t.title, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14, color: AppColors.text), maxLines: 2, overflow: TextOverflow.ellipsis)), const SizedBox(width: 8), // Tappable status badge → inline status picker GestureDetector( onTap: _updatingStatus ? null : _showStatusPicker, child: _updatingStatus ? const SizedBox( width: 60, height: 22, child: Center(child: SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2), )), ) : Row(mainAxisSize: MainAxisSize.min, children: [ StatusBadge(t.status, small: true), const SizedBox(width: 2), const Icon(Icons.arrow_drop_down, size: 16, color: AppColors.textSub), ]), ), ]), const SizedBox(height: 8), Wrap( spacing: 12, runSpacing: 4, children: [ if (t.assignedUsername != null) _meta(Icons.person_outline, t.assignedUsername!), if (t.dueDate != null) _meta( Icons.calendar_today_outlined, DateFormat('d MMM yyyy').format(t.dueDate!), color: t.isOverdue ? AppColors.danger : AppColors.textSub, ), if (t.subtaskCount > 0) _meta(Icons.subdirectory_arrow_right, '${t.subtaskCount} subtask'), if (t.commentCount > 0) _meta(Icons.chat_bubble_outline, '${t.commentCount} comments'), ], ), ], ), ), ); } Widget _meta(IconData icon, String text, {Color? color}) => Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 13, color: color ?? AppColors.textSub), const SizedBox(width: 4), Text(text, style: TextStyle(fontSize: 12, color: color ?? AppColors.textSub)), ], ); }