Add full CRUD tasks with AI integration

This commit is contained in:
2026-03-27 18:57:56 +05:00
parent c5072a8a84
commit 2336df8e86
4 changed files with 968 additions and 53 deletions
+25
View File
@@ -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<String, dynamic>?;
return TaskFormScreen(
projectId: args?['projectId'] ?? 'default',
);
},
),
GoRoute(
path: ':taskId',
name: 'edit-task',
builder: (context, state) {
final args = state.extra as Map<String, dynamic>?;
final task = args?['task'];
return TaskFormScreen(
task: task,
projectId: args?['projectId'] ?? 'default',
);
},
),
],
),
GoRoute(
path: 'mindmap',
+210
View File
@@ -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<TasksNotifier, AsyncValue<List<TaskModel>>>((ref) {
return TasksNotifier();
});
// Provider для задач конкретного проекта
final tasksByProjectProvider = Provider.family<AsyncValue<List<TaskModel>>, 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<AsyncValue<List<TaskModel>>, 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<AsyncValue<List<TaskModel>>> {
TasksNotifier() : super(const AsyncValue.loading()) {
_loadTasks();
}
// Загрузка задач
Future<void> _loadTasks() async {
try {
// Сначала загружаем из локального хранилища
final localTasks = localStorageService.getTasks();
state = AsyncValue.data(localTasks);
// Потом синхронизируем с Firestore
// TODO: Реализовать когда будет Firebase Auth
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
// Создание задачи
Future<TaskModel> 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<void> 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<void> 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<List<TaskModel>> 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 = <TaskModel>[];
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<TaskPriority> prioritizeTaskWithAI({
required String title,
required String description,
DateTime? dueDate,
}) async {
return await deepSeekService.prioritizeTask(
title: title,
description: description,
dueDate: dueDate,
);
}
// Фильтрация задач по статусу
List<TaskModel> filterByStatus(List<TaskModel> tasks, TaskStatus status) {
return tasks.where((t) => t.status == status).toList();
}
// Фильтрация задач по приоритету
List<TaskModel> filterByPriority(List<TaskModel> tasks, TaskPriority priority) {
return tasks.where((t) => t.priority == priority).toList();
}
// Просроченные задачи
List<TaskModel> getOverdueTasks(List<TaskModel> tasks) {
return tasks.where((t) => t.isOverdue).toList();
}
}
+416
View File
@@ -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<TaskFormScreen> createState() => _TaskFormScreenState();
}
class _TaskFormScreenState extends ConsumerState<TaskFormScreen> {
final _formKey = GlobalKey<FormState>();
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<void> _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<void> _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<void> _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<void> _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<TaskPriority>(
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('Удалить'),
),
],
),
);
}
}
+313 -49
View File
@@ -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(
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),
itemCount: 10,
itemBuilder: (context, index) {
return _TaskCard(index: index);
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,
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
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),
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('Создать задачу'),
),
],
),
],
),
trailing: PopupMenuButton(
itemBuilder: (context) => [
const PopupMenuItem(value: 'edit', child: Text('Редактировать')),
const PopupMenuItem(value: 'delete', child: Text('Удалить')),
],
),
isThreeLine: true,
),
);
}
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<String>(
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('Удалить'),
),
],
),
);
}
}