diff --git a/lib/features/tasks/presentation/pages/calendar_page.dart b/lib/features/tasks/presentation/pages/calendar_page.dart index 5ebda13..4f982c9 100644 --- a/lib/features/tasks/presentation/pages/calendar_page.dart +++ b/lib/features/tasks/presentation/pages/calendar_page.dart @@ -2,6 +2,12 @@ import 'package:flutter/material.dart'; import 'package:agenda_tasks/l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +import '../../../../core/di/injection_container.dart'; +import '../../domain/entities/task_entity.dart'; +import '../../domain/enums/priority.dart'; +import '../../domain/repositories/task_repository.dart'; class CalendarPage extends StatefulWidget { const CalendarPage({super.key}); @@ -13,11 +19,60 @@ class CalendarPage extends StatefulWidget { class _CalendarPageState extends State { late DateTime _focusedMonth; DateTime? _selectedDate; + Map> _tasksByDate = {}; + bool _isLoading = false; @override void initState() { super.initState(); _focusedMonth = DateTime.now(); + _loadTasksForMonth(); + } + + Future _loadTasksForMonth() async { + setState(() => _isLoading = true); + + final repository = getIt(); + final Map> tasks = {}; + + final firstDay = DateTime(_focusedMonth.year, _focusedMonth.month, 1); + final lastDay = DateTime(_focusedMonth.year, _focusedMonth.month + 1, 0); + + for (int day = 1; day <= lastDay.day; day++) { + final date = DateTime(_focusedMonth.year, _focusedMonth.month, day); + final dateStr = DateFormat('yyyy-MM-dd').format(date); + final result = await repository.getTasksByDate(date); + result.when( + success: (data) { + if (data.isNotEmpty) { + tasks[dateStr] = data; + } + }, + error: (_) {}, + ); + } + + if (mounted) { + setState(() { + _tasksByDate = tasks; + _isLoading = false; + }); + } + } + + Priority? _getHighestPriority(List? tasks) { + if (tasks == null || tasks.isEmpty) return null; + + final incompleteTasks = tasks.where((t) => !t.isDone).toList(); + if (incompleteTasks.isEmpty) return null; + + if (incompleteTasks.any((t) => t.priority == Priority.high)) { + return Priority.high; + } + if (incompleteTasks.any((t) => t.priority == Priority.medium)) { + return Priority.medium; + } + return Priority.low; } @override @@ -42,22 +97,28 @@ class _CalendarPageState extends State { setState(() { _focusedMonth = DateTime(_focusedMonth.year, _focusedMonth.month - 1); }); + _loadTasksForMonth(); }, onNext: () { setState(() { _focusedMonth = DateTime(_focusedMonth.year, _focusedMonth.month + 1); }); + _loadTasksForMonth(); }, ), _WeekdayHeaders(locale: locale), Expanded( - child: _CalendarGrid( - focusedMonth: _focusedMonth, - selectedDate: _selectedDate, - onDateSelected: (date) { - setState(() => _selectedDate = date); - }, - ), + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _CalendarGrid( + focusedMonth: _focusedMonth, + selectedDate: _selectedDate, + tasksByDate: _tasksByDate, + getHighestPriority: _getHighestPriority, + onDateSelected: (date) { + setState(() => _selectedDate = date); + }, + ), ), ], ), @@ -154,11 +215,15 @@ class _WeekdayHeaders extends StatelessWidget { class _CalendarGrid extends StatelessWidget { final DateTime focusedMonth; final DateTime? selectedDate; + final Map> tasksByDate; + final Priority? Function(List?) getHighestPriority; final ValueChanged onDateSelected; const _CalendarGrid({ required this.focusedMonth, required this.selectedDate, + required this.tasksByDate, + required this.getHighestPriority, required this.onDateSelected, }); @@ -185,6 +250,7 @@ class _CalendarGrid extends StatelessWidget { } final date = DateTime(focusedMonth.year, focusedMonth.month, dayOffset + 1); + final dateStr = DateFormat('yyyy-MM-dd').format(date); final isToday = date.year == today.year && date.month == today.month && date.day == today.day; @@ -193,6 +259,9 @@ class _CalendarGrid extends StatelessWidget { date.month == selectedDate!.month && date.day == selectedDate!.day; + final tasks = tasksByDate[dateStr]; + final highestPriority = getHighestPriority(tasks); + return GestureDetector( onTap: () => onDateSelected(date), child: Container( @@ -205,18 +274,98 @@ class _CalendarGrid extends StatelessWidget { : null, borderRadius: BorderRadius.circular(8), ), - child: Center( - child: Text( - '${dayOffset + 1}', - style: TextStyle( - color: isSelected - ? Theme.of(context).colorScheme.onPrimary - : isToday - ? Theme.of(context).colorScheme.onPrimaryContainer - : null, - fontWeight: isToday || isSelected ? FontWeight.bold : null, + child: Stack( + alignment: Alignment.center, + children: [ + Text( + '${dayOffset + 1}', + style: TextStyle( + color: isSelected + ? Theme.of(context).colorScheme.onPrimary + : isToday + ? Theme.of(context).colorScheme.onPrimaryContainer + : null, + fontWeight: isToday || isSelected ? FontWeight.bold : null, + ), ), - ), + if (highestPriority != null) + Positioned( + bottom: 4, + child: _PulsingDot(priority: highestPriority), + ), + ], + ), + ), + ); + }, + ); + } +} + +class _PulsingDot extends StatefulWidget { + final Priority priority; + + const _PulsingDot({required this.priority}); + + @override + State<_PulsingDot> createState() => _PulsingDotState(); +} + +class _PulsingDotState extends State<_PulsingDot> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + )..repeat(reverse: true); + + _animation = Tween(begin: 0.6, end: 1.0).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Color _getPriorityColor() { + switch (widget.priority) { + case Priority.high: + return Colors.red.shade600; + case Priority.medium: + return Colors.orange.shade600; + case Priority.low: + return Colors.green.shade600; + } + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Opacity( + opacity: _animation.value, + child: Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: _getPriorityColor(), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: _getPriorityColor().withOpacity(0.5), + blurRadius: 2, + spreadRadius: 1, + ), + ], ), ), ); diff --git a/lib/features/tasks/presentation/pages/daily_agenda_page.dart b/lib/features/tasks/presentation/pages/daily_agenda_page.dart index 13e39ae..2ff3200 100644 --- a/lib/features/tasks/presentation/pages/daily_agenda_page.dart +++ b/lib/features/tasks/presentation/pages/daily_agenda_page.dart @@ -16,7 +16,9 @@ class DailyAgendaPage extends StatelessWidget { @override Widget build(BuildContext context) { + // Use key to force rebuild when date changes return ChangeNotifierProvider( + key: ValueKey(initialDate), create: (_) { final vm = getIt(); if (initialDate != null) {