351 lines
11 KiB
Dart
351 lines
11 KiB
Dart
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 ConsumerWidget {
|
||
const TasksScreen({super.key});
|
||
|
||
@override
|
||
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: () => _showFilterDialog(context),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.search),
|
||
onPressed: () {},
|
||
),
|
||
],
|
||
),
|
||
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: () 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('Новая задача'),
|
||
),
|
||
);
|
||
}
|
||
|
||
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('Создать задачу'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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('Удалить'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|