diff --git a/lib/core/router.dart b/lib/core/router.dart index d005649..04bd930 100644 --- a/lib/core/router.dart +++ b/lib/core/router.dart @@ -2,6 +2,7 @@ import 'package:go_router/go_router.dart'; import '../screens/auth_screen.dart'; import '../screens/home_screen.dart'; import '../screens/tasks_screen.dart'; +import '../screens/task_form_screen.dart'; import '../screens/mind_map_screen.dart'; import '../screens/calendar_screen.dart'; import '../screens/alarms_screen.dart'; @@ -25,6 +26,30 @@ class AppRouter { path: 'tasks', name: 'tasks', builder: (context, state) => const TasksScreen(), + routes: [ + GoRoute( + path: 'new', + name: 'new-task', + builder: (context, state) { + final args = state.extra as Map?; + return TaskFormScreen( + projectId: args?['projectId'] ?? 'default', + ); + }, + ), + GoRoute( + path: ':taskId', + name: 'edit-task', + builder: (context, state) { + final args = state.extra as Map?; + final task = args?['task']; + return TaskFormScreen( + task: task, + projectId: args?['projectId'] ?? 'default', + ); + }, + ), + ], ), GoRoute( path: 'mindmap', diff --git a/lib/providers/task_provider.dart b/lib/providers/task_provider.dart new file mode 100644 index 0000000..e41f113 --- /dev/null +++ b/lib/providers/task_provider.dart @@ -0,0 +1,210 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:uuid/uuid.dart'; +import '../models/task_model.dart'; +import '../models/project_model.dart'; +import '../core/firestore_service.dart'; +import '../core/local_storage_service.dart'; +import '../core/deepseek_service.dart'; + +// Provider для списка всех задач +final tasksProvider = StateNotifierProvider>>((ref) { + return TasksNotifier(); +}); + +// Provider для задач конкретного проекта +final tasksByProjectProvider = Provider.family>, String>((ref, projectId) { + final tasks = ref.watch(tasksProvider); + return tasks.when( + data: (allTasks) => AsyncValue.data(allTasks.where((t) => t.projectId == projectId).toList()), + loading: () => const AsyncValue.loading(), + error: (e, st) => AsyncValue.error(e, st), + ); +}); + +// Provider для задач пользователя +final tasksByUserProvider = Provider.family>, String>((ref, userId) { + final tasks = ref.watch(tasksProvider); + return tasks.when( + data: (allTasks) => AsyncValue.data(allTasks.where((t) => t.assigneeId == userId).toList()), + loading: () => const AsyncValue.loading(), + error: (e, st) => AsyncValue.error(e, st), + ); +}); + +class TasksNotifier extends StateNotifier>> { + TasksNotifier() : super(const AsyncValue.loading()) { + _loadTasks(); + } + + // Загрузка задач + Future _loadTasks() async { + try { + // Сначала загружаем из локального хранилища + final localTasks = localStorageService.getTasks(); + state = AsyncValue.data(localTasks); + + // Потом синхронизируем с Firestore + // TODO: Реализовать когда будет Firebase Auth + } catch (e, st) { + state = AsyncValue.error(e, st); + } + } + + // Создание задачи + Future createTask({ + required String title, + String? description, + required String projectId, + String? parentTaskId, + TaskPriority priority = TaskPriority.medium, + DateTime? dueDate, + int estimatedHours = 0, + }) async { + try { + final task = TaskModel( + id: const Uuid().v4(), + title: title, + description: description, + projectId: projectId, + parentTaskId: parentTaskId, + priority: priority, + dueDate: dueDate, + estimatedHours: estimatedHours, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); + + // Сохраняем локально + await localStorageService.saveTask(task); + + // Обновляем состояние + final currentTasks = state.value ?? []; + state = AsyncValue.data([...currentTasks, task]); + + // TODO: Сохранить в Firestore когда будет авторизация + // await firestoreService.createTask(task); + + return task; + } catch (e, st) { + state = AsyncValue.error(e, st); + rethrow; + } + } + + // Обновление задачи + Future updateTask(String taskId, { + String? title, + String? description, + TaskStatus? status, + TaskPriority? priority, + DateTime? dueDate, + String? assigneeId, + }) async { + try { + final currentTasks = state.value ?? []; + final taskIndex = currentTasks.indexWhere((t) => t.id == taskId); + + if (taskIndex == -1) throw Exception('Task not found'); + + final oldTask = currentTasks[taskIndex]; + final updatedTask = oldTask.copyWith( + title: title ?? oldTask.title, + description: description ?? oldTask.description, + status: status ?? oldTask.status, + priority: priority ?? oldTask.priority, + dueDate: dueDate ?? oldTask.dueDate, + assigneeId: assigneeId ?? oldTask.assigneeId, + updatedAt: DateTime.now(), + ); + + // Сохраняем локально + await localStorageService.saveTask(updatedTask); + + // Обновляем состояние + currentTasks[taskIndex] = updatedTask; + state = AsyncValue.data(List.from(currentTasks)); + + // TODO: Обновить в Firestore + } catch (e, st) { + state = AsyncValue.error(e, st); + rethrow; + } + } + + // Удаление задачи + Future deleteTask(String taskId) async { + try { + // Удаляем локально + await localStorageService.deleteTask(taskId); + + // Обновляем состояние + final currentTasks = state.value ?? []; + currentTasks.removeWhere((t) => t.id == taskId); + state = AsyncValue.data(List.from(currentTasks)); + + // TODO: Удалить из Firestore + } catch (e, st) { + state = AsyncValue.error(e, st); + rethrow; + } + } + + // AI: Генерация подзадач + Future> generateSubtasksWithAI({ + required String title, + required String description, + required String projectId, + String? parentTaskId, + }) async { + try { + final subtasksData = await deepSeekService.generateSubtasks( + title: title, + description: description, + ); + + final subtasks = []; + for (final subtaskData in subtasksData) { + final subtask = await createTask( + title: subtaskData['title'] ?? '', + description: subtaskData['description'], + projectId: projectId, + parentTaskId: parentTaskId, + estimatedHours: subtaskData['estimatedHours'] ?? 0, + ); + subtasks.add(subtask); + } + + return subtasks; + } catch (e, st) { + rethrow; + } + } + + // AI: Приоритизация задачи + Future prioritizeTaskWithAI({ + required String title, + required String description, + DateTime? dueDate, + }) async { + return await deepSeekService.prioritizeTask( + title: title, + description: description, + dueDate: dueDate, + ); + } + + // Фильтрация задач по статусу + List filterByStatus(List tasks, TaskStatus status) { + return tasks.where((t) => t.status == status).toList(); + } + + // Фильтрация задач по приоритету + List filterByPriority(List tasks, TaskPriority priority) { + return tasks.where((t) => t.priority == priority).toList(); + } + + // Просроченные задачи + List getOverdueTasks(List tasks) { + return tasks.where((t) => t.isOverdue).toList(); + } +} diff --git a/lib/screens/task_form_screen.dart b/lib/screens/task_form_screen.dart new file mode 100644 index 0000000..7260bb7 --- /dev/null +++ b/lib/screens/task_form_screen.dart @@ -0,0 +1,416 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../providers/task_provider.dart'; +import '../models/task_model.dart'; + +class TaskFormScreen extends ConsumerStatefulWidget { + final TaskModel? task; + final String projectId; + + const TaskFormScreen({ + super.key, + this.task, + required this.projectId, + }); + + @override + ConsumerState createState() => _TaskFormScreenState(); +} + +class _TaskFormScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _titleController = TextEditingController(); + final _descriptionController = TextEditingController(); + + TaskPriority _priority = TaskPriority.medium; + DateTime? _dueDate; + int _estimatedHours = 0; + bool _isGenerating = false; + + @override + void initState() { + super.initState(); + if (widget.task != null) { + _titleController.text = widget.task!.title; + _descriptionController.text = widget.task!.description ?? ''; + _priority = widget.task!.priority; + _dueDate = widget.task!.dueDate; + _estimatedHours = widget.task!.estimatedHours; + } + } + + @override + void dispose() { + _titleController.dispose(); + _descriptionController.dispose(); + super.dispose(); + } + + Future _selectDueDate() async { + final picked = await showDatePicker( + context: context, + initialDate: _dueDate ?? DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + + if (picked != null) { + setState(() { + _dueDate = picked; + }); + } + } + + Future _generateSubtasksWithAI() async { + if (_titleController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Введите название задачи')), + ); + return; + } + + setState(() { + _isGenerating = true; + }); + + try { + await ref.read(tasksProvider.notifier).generateSubtasksWithAI( + title: _titleController.text, + description: _descriptionController.text, + projectId: widget.projectId, + parentTaskId: widget.task?.id, + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('✅ Подзадачи успешно сгенерированы!'), + backgroundColor: Colors.green, + ), + ); + context.pop(true); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('❌ Ошибка: $e'), + backgroundColor: Colors.red, + ), + ); + } + } finally { + setState(() { + _isGenerating = false; + }); + } + } + + Future _prioritizeWithAI() async { + if (_titleController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Введите название задачи')), + ); + return; + } + + setState(() { + _isGenerating = true; + }); + + try { + final priority = await ref.read(tasksProvider.notifier).prioritizeTaskWithAI( + title: _titleController.text, + description: _descriptionController.text, + dueDate: _dueDate, + ); + + setState(() { + _priority = priority; + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('✅ AI определил приоритет: ${priority.name}'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('❌ Ошибка: $e'), + backgroundColor: Colors.red, + ), + ); + } + } finally { + setState(() { + _isGenerating = false; + }); + } + } + + Future _saveTask() async { + if (!_formKey.currentState!.validate()) return; + + try { + if (widget.task != null) { + // Редактирование + await ref.read(tasksProvider.notifier).updateTask( + widget.task!.id, + title: _titleController.text, + description: _descriptionController.text, + priority: _priority, + dueDate: _dueDate, + ); + } else { + // Создание + await ref.read(tasksProvider.notifier).createTask( + title: _titleController.text, + description: _descriptionController.text, + projectId: widget.projectId, + priority: _priority, + dueDate: _dueDate, + estimatedHours: _estimatedHours, + ); + } + + if (mounted) { + context.pop(true); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('❌ Ошибка: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + final isEdit = widget.task != null; + + return Scaffold( + appBar: AppBar( + title: Text(isEdit ? 'Редактировать задачу' : 'Новая задача'), + actions: [ + if (isEdit) + IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: () => _confirmDelete(), + tooltip: 'Удалить', + ), + ], + ), + body: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + // Название + TextFormField( + controller: _titleController, + decoration: const InputDecoration( + labelText: 'Название *', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.title), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Введите название'; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Описание + TextFormField( + controller: _descriptionController, + decoration: const InputDecoration( + labelText: 'Описание', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.description), + alignLabelWithHint: true, + ), + maxLines: 5, + ), + const SizedBox(height: 24), + + // AI Кнопки + if (!isEdit) ...[ + _buildAIButton( + icon: Icons.auto_awesome, + label: 'AI: Создать подзадачи', + onPressed: _isGenerating ? null : _generateSubtasksWithAI, + color: Colors.purple, + ), + const SizedBox(height: 8), + _buildAIButton( + icon: Icons.priority_high, + label: 'AI: Определить приоритет', + onPressed: _isGenerating ? null : _prioritizeWithAI, + color: Colors.orange, + ), + const SizedBox(height: 24), + ], + + // Приоритет + DropdownButtonFormField( + value: _priority, + decoration: const InputDecoration( + labelText: 'Приоритет', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.flag), + ), + items: TaskPriority.values.map((priority) { + return DropdownMenuItem( + value: priority, + child: Row( + children: [ + _getPriorityIcon(priority), + const SizedBox(width: 8), + Text(_getPriorityLabel(priority)), + ], + ), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _priority = value; + }); + } + }, + ), + const SizedBox(height: 16), + + // Дедлайн + ListTile( + leading: const Icon(Icons.calendar_today), + title: const Text('Дедлайн'), + subtitle: Text(_dueDate != null + ? '📅 ${_dueDate!.day}.${_dueDate!.month}.${_dueDate!.year}' + : 'Не выбран'), + trailing: const Icon(Icons.chevron_right), + onTap: _selectDueDate, + ), + const SizedBox(height: 16), + + // Оценка времени + Row( + children: [ + Expanded( + child: TextFormField( + initialValue: _estimatedHours.toString(), + decoration: const InputDecoration( + labelText: 'Оценка (часы)', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.timer), + ), + keyboardType: TextInputType.number, + onChanged: (value) { + _estimatedHours = int.tryParse(value) ?? 0; + }, + ), + ), + ], + ), + const SizedBox(height: 32), + + // Кнопка сохранения + ElevatedButton( + onPressed: _saveTask, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: _isGenerating + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(isEdit ? 'Сохранить' : 'Создать задачу'), + ), + ], + ), + ), + ); + } + + Widget _buildAIButton({ + required IconData icon, + required String label, + required VoidCallback? onPressed, + required Color color, + }) { + return OutlinedButton.icon( + onPressed: onPressed, + icon: Icon(icon, color: color), + label: Text(label), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide(color: color), + ), + ); + } + + Widget _getPriorityIcon(TaskPriority priority) { + switch (priority) { + case TaskPriority.urgent: + return const Icon(Icons.fire_engine, color: Colors.red); + case TaskPriority.high: + return const Icon(Icons.arrow_upward, color: Colors.orange); + case TaskPriority.medium: + return const Icon(Icons.remove, color: Colors.blue); + case TaskPriority.low: + return const Icon(Icons.arrow_downward, color: Colors.green); + } + } + + String _getPriorityLabel(TaskPriority priority) { + switch (priority) { + case TaskPriority.urgent: + return 'Срочно'; + case TaskPriority.high: + return 'Высокий'; + case TaskPriority.medium: + return 'Средний'; + case TaskPriority.low: + return 'Низкий'; + } + } + + void _confirmDelete() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Удалить задачу?'), + content: const Text('Это действие нельзя отменить'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Отмена'), + ), + TextButton( + onPressed: () { + ref.read(tasksProvider.notifier).deleteTask(widget.task!.id); + Navigator.pop(context); + context.pop(true); + }, + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Удалить'), + ), + ], + ), + ); + } +} diff --git a/lib/screens/tasks_screen.dart b/lib/screens/tasks_screen.dart index 4e100dd..0f93e05 100644 --- a/lib/screens/tasks_screen.dart +++ b/lib/screens/tasks_screen.dart @@ -1,17 +1,23 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../providers/task_provider.dart'; +import '../models/task_model.dart'; -class TasksScreen extends StatelessWidget { +class TasksScreen extends ConsumerWidget { const TasksScreen({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final tasksState = ref.watch(tasksProvider); + return Scaffold( appBar: AppBar( title: const Text('Задачи'), actions: [ IconButton( icon: const Icon(Icons.filter_list), - onPressed: () {}, + onPressed: () => _showFilterDialog(context), ), IconButton( icon: const Icon(Icons.search), @@ -19,68 +25,326 @@ class TasksScreen extends StatelessWidget { ), ], ), - body: ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: 10, - itemBuilder: (context, index) { - return _TaskCard(index: index); + body: tasksState.when( + data: (tasks) { + if (tasks.isEmpty) { + return _buildEmptyState(context); + } + + // Группируем по статусам + final todoTasks = + tasks.where((t) => t.status == TaskStatus.todo).toList(); + final inProgressTasks = + tasks.where((t) => t.status == TaskStatus.inProgress).toList(); + final doneTasks = + tasks.where((t) => t.status == TaskStatus.done).toList(); + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + if (todoTasks.isNotEmpty) ...[ + _buildSectionHeader('В планах', todoTasks.length, Colors.blue), + ...todoTasks.map((task) => _TaskCard(task: task)), + const SizedBox(height: 16), + ], + if (inProgressTasks.isNotEmpty) ...[ + _buildSectionHeader( + 'В работе', inProgressTasks.length, Colors.orange), + ...inProgressTasks.map((task) => _TaskCard(task: task)), + const SizedBox(height: 16), + ], + if (doneTasks.isNotEmpty) ...[ + _buildSectionHeader('Готово', doneTasks.length, Colors.green), + ...doneTasks.map((task) => _TaskCard(task: task)), + ], + ], + ); }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 48, color: Colors.red), + const SizedBox(height: 16), + Text('Ошибка: $error'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => ref.refresh(tasksProvider), + child: const Text('Повторить'), + ), + ], + ), + ), ), floatingActionButton: FloatingActionButton.extended( - onPressed: () {}, + onPressed: () async { + final result = await context + .push('/home/tasks/new', extra: {'projectId': 'default'}); + if (result == true) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('✅ Задача создана')), + ); + } + }, icon: const Icon(Icons.add), label: const Text('Новая задача'), ), ); } -} -class _TaskCard extends StatelessWidget { - final int index; - - const _TaskCard({required this.index}); - - @override - Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: ListTile( - leading: CircleAvatar( - backgroundColor: _getPriorityColor(index), - child: Text('${index + 1}'), - ), - title: Text('Задача #${index + 1}'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 4), - Text('Описание задачи...'), - const SizedBox(height: 4), - Row( - children: [ - Icon(Icons.calendar_today, size: 12, color: Colors.grey), - const SizedBox(width: 4), - Text( - 'До: ${DateTime.now().add(Duration(days: index)).toString().split(' ')[0]}', - style: const TextStyle(fontSize: 12, color: Colors.grey), - ), - ], - ), - ], - ), - trailing: PopupMenuButton( - itemBuilder: (context) => [ - const PopupMenuItem(value: 'edit', child: Text('Редактировать')), - const PopupMenuItem(value: 'delete', child: Text('Удалить')), - ], - ), - isThreeLine: true, + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.task_alt, + size: 100, + color: Colors.grey, + ), + const SizedBox(height: 24), + const Text( + 'Нет задач', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + 'Создайте первую задачу', + style: TextStyle(color: Colors.grey), + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: () => context + .push('/home/tasks/new', extra: {'projectId': 'default'}), + icon: const Icon(Icons.add), + label: const Text('Создать задачу'), + ), + ], ), ); } - Color _getPriorityColor(int index) { - final colors = [Colors.red, Colors.orange, Colors.blue, Colors.green]; - return colors[index % colors.length]; + Widget _buildSectionHeader(String title, int count, Color color) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: color.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '$count', + style: TextStyle(color: color, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ); + } + + void _showFilterDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Фильтр задач'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.all_inclusive), + title: const Text('Все задачи'), + onTap: () => Navigator.pop(context), + ), + ListTile( + leading: const Icon(Icons.pending, color: Colors.blue), + title: const Text('В планах'), + onTap: () => Navigator.pop(context), + ), + ListTile( + leading: const Icon(Icons.hourglass_empty, color: Colors.orange), + title: const Text('В работе'), + onTap: () => Navigator.pop(context), + ), + ListTile( + leading: const Icon(Icons.check_circle, color: Colors.green), + title: const Text('Готово'), + onTap: () => Navigator.pop(context), + ), + ], + ), + ), + ); + } +} + +class _TaskCard extends ConsumerWidget { + final TaskModel task; + + const _TaskCard({required this.task}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + leading: CircleAvatar( + backgroundColor: _getPriorityColor(task.priority), + child: Icon( + _getPriorityIcon(task.priority), + color: Colors.white, + size: 20, + ), + ), + title: Text( + task.title, + style: TextStyle( + decoration: task.status == TaskStatus.done + ? TextDecoration.lineThrough + : null, + color: task.status == TaskStatus.done ? Colors.grey : null, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (task.description != null && task.description!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + task.description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + const SizedBox(height: 4), + Row( + children: [ + if (task.dueDate != null) ...[ + Icon( + task.isOverdue ? Icons.warning : Icons.calendar_today, + size: 12, + color: task.isOverdue ? Colors.red : Colors.grey, + ), + const SizedBox(width: 4), + Text( + '${task.dueDate!.day}.${task.dueDate!.month}', + style: TextStyle( + fontSize: 12, + color: task.isOverdue ? Colors.red : Colors.grey, + ), + ), + const SizedBox(width: 12), + ], + if (task.estimatedHours > 0) ...[ + const Icon(Icons.timer, size: 12, color: Colors.grey), + const SizedBox(width: 4), + Text( + '${task.estimatedHours}ч', + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ], + ), + ], + ), + trailing: PopupMenuButton( + onSelected: (value) => _handleMenuAction(context, ref, value), + itemBuilder: (context) => [ + const PopupMenuItem(value: 'edit', child: Text('Редактировать')), + const PopupMenuItem(value: 'duplicate', child: Text('Дублировать')), + const PopupMenuItem(value: 'delete', child: Text('Удалить')), + ], + ), + isThreeLine: true, + onTap: () => context.push('/home/tasks/${task.id}', extra: { + 'task': task, + 'projectId': task.projectId, + }), + ), + ); + } + + Color _getPriorityColor(TaskPriority priority) { + switch (priority) { + case TaskPriority.urgent: + return Colors.red; + case TaskPriority.high: + return Colors.orange; + case TaskPriority.medium: + return Colors.blue; + case TaskPriority.low: + return Colors.green; + } + } + + IconData _getPriorityIcon(TaskPriority priority) { + switch (priority) { + case TaskPriority.urgent: + return Icons.fire_engine; + case TaskPriority.high: + return Icons.arrow_upward; + case TaskPriority.medium: + return Icons.remove; + case TaskPriority.low: + return Icons.arrow_downward; + } + } + + void _handleMenuAction(BuildContext context, WidgetRef ref, String action) { + switch (action) { + case 'edit': + context.push('/home/tasks/${task.id}', extra: { + 'task': task, + 'projectId': task.projectId, + }); + break; + case 'delete': + _confirmDelete(context, ref); + break; + } + } + + void _confirmDelete(BuildContext context, WidgetRef ref) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Удалить задачу?'), + content: const Text('Это действие нельзя отменить'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Отмена'), + ), + TextButton( + onPressed: () { + ref.read(tasksProvider.notifier).deleteTask(task.id); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('✅ Задача удалена')), + ); + }, + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Удалить'), + ), + ], + ), + ); } }