Files
Avtex/lib/screens/tasks_screen.dart
T

351 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('Удалить'),
),
],
),
);
}
}