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
+317 -53
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(
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<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('Удалить'),
),
],
),
);
}
}