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 '../../../../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}); @override State createState() => _CalendarPageState(); } 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 lastDay = DateTime(_focusedMonth.year, _focusedMonth.month + 1, 0); // Create all date requests final dates = List.generate( lastDay.day, (index) => DateTime(_focusedMonth.year, _focusedMonth.month, index + 1), ); // Execute all API calls in parallel final results = await Future.wait( dates.map((date) => repository.getTasksByDate(date)), ); // Process results for (int i = 0; i < dates.length; i++) { final dateStr = DateFormat('yyyy-MM-dd').format(dates[i]); results[i].when( success: (data) { if (data.isNotEmpty) { tasks[dateStr] = data; } }, error: (_) {}, ); } if (mounted) { setState(() { _tasksByDate = tasks; _isLoading = false; }); } } List _getUniquePriorities(List? tasks) { if (tasks == null || tasks.isEmpty) return []; final incompleteTasks = tasks.where((t) => !t.isDone).toList(); if (incompleteTasks.isEmpty) return []; // Get unique priorities, sorted by importance (high first) final priorities = []; if (incompleteTasks.any((t) => t.priority == Priority.high)) { priorities.add(Priority.high); } if (incompleteTasks.any((t) => t.priority == Priority.medium)) { priorities.add(Priority.medium); } if (incompleteTasks.any((t) => t.priority == Priority.low)) { priorities.add(Priority.low); } return priorities; } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final locale = Localizations.localeOf(context).languageCode; return Scaffold( appBar: AppBar( title: Text(l10n.calendar), leading: IconButton( icon: const Icon(Icons.close), onPressed: () => context.pop(), ), ), body: Column( children: [ _MonthNavigation( month: _focusedMonth, locale: locale, onPrevious: () { setState(() { _focusedMonth = DateTime(_focusedMonth.year, _focusedMonth.month - 1); }); _loadTasksForMonth(); }, onNext: () { setState(() { _focusedMonth = DateTime(_focusedMonth.year, _focusedMonth.month + 1); }); _loadTasksForMonth(); }, ), _WeekdayHeaders(locale: locale), Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) : _CalendarGrid( focusedMonth: _focusedMonth, selectedDate: _selectedDate, tasksByDate: _tasksByDate, getUniquePriorities: _getUniquePriorities, onDateSelected: (date) { setState(() => _selectedDate = date); }, ), ), ], ), bottomNavigationBar: _selectedDate != null ? SafeArea( child: Padding( padding: const EdgeInsets.all(16), child: SizedBox( height: 56, child: ElevatedButton( onPressed: () { final dateStr = DateFormat('yyyy-MM-dd').format(_selectedDate!); context.go('/?date=$dateStr'); }, child: Text(l10n.goToDay), ), ), ), ) : null, ); } } class _MonthNavigation extends StatelessWidget { final DateTime month; final String locale; final VoidCallback onPrevious; final VoidCallback onNext; const _MonthNavigation({ required this.month, required this.locale, required this.onPrevious, required this.onNext, }); @override Widget build(BuildContext context) { final monthFormat = DateFormat.yMMMM(locale); return Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: 48, height: 48, child: IconButton( icon: const Icon(Icons.chevron_left, size: 28), onPressed: onPrevious, ), ), Text( monthFormat.format(month), style: Theme.of(context).textTheme.titleLarge, ), SizedBox( width: 48, height: 48, child: IconButton( icon: const Icon(Icons.chevron_right, size: 28), onPressed: onNext, ), ), ], ), ); } } class _WeekdayHeaders extends StatelessWidget { final String locale; const _WeekdayHeaders({required this.locale}); @override Widget build(BuildContext context) { final weekdays = DateFormat.E(locale); final today = DateTime.now(); final monday = today.subtract(Duration(days: today.weekday - 1)); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( children: List.generate(7, (index) { final day = monday.add(Duration(days: index)); return Expanded( child: Center( child: Text( weekdays.format(day).substring(0, 2), style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: FontWeight.bold, ), ), ), ); }), ), ); } } class _CalendarGrid extends StatelessWidget { final DateTime focusedMonth; final DateTime? selectedDate; final Map> tasksByDate; final List Function(List?) getUniquePriorities; final ValueChanged onDateSelected; const _CalendarGrid({ required this.focusedMonth, required this.selectedDate, required this.tasksByDate, required this.getUniquePriorities, required this.onDateSelected, }); @override Widget build(BuildContext context) { final firstDayOfMonth = DateTime(focusedMonth.year, focusedMonth.month, 1); final lastDayOfMonth = DateTime(focusedMonth.year, focusedMonth.month + 1, 0); final startOffset = (firstDayOfMonth.weekday - 1) % 7; final totalDays = startOffset + lastDayOfMonth.day; final rowCount = (totalDays / 7).ceil(); final today = DateTime.now(); return GridView.builder( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 7, childAspectRatio: 1, mainAxisSpacing: 4, crossAxisSpacing: 4, ), itemCount: rowCount * 7, itemBuilder: (context, index) { final dayOffset = index - startOffset; if (dayOffset < 0 || dayOffset >= lastDayOfMonth.day) { return const SizedBox(); } 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; final isSelected = selectedDate != null && date.year == selectedDate!.year && date.month == selectedDate!.month && date.day == selectedDate!.day; final tasks = tasksByDate[dateStr]; final priorities = getUniquePriorities(tasks); return GestureDetector( onTap: () => onDateSelected(date), child: Container( decoration: BoxDecoration( color: isSelected ? Theme.of(context).colorScheme.primary : isToday ? Theme.of(context).colorScheme.primaryContainer : null, borderRadius: BorderRadius.circular(8), ), 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 (priorities.isNotEmpty) Positioned( bottom: 6, child: _PriorityDots(priorities: priorities), ), ], ), ), ); }, ); } } class _PriorityDots extends StatelessWidget { final List priorities; const _PriorityDots({required this.priorities}); @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: priorities .map((priority) => Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: _PulsingDot(priority: priority), )) .toList(), ); } } 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: 10, height: 10, decoration: BoxDecoration( color: _getPriorityColor(), shape: BoxShape.circle, boxShadow: [ BoxShadow( color: _getPriorityColor().withValues(alpha: 0.5), blurRadius: 3, spreadRadius: 1, ), ], ), ), ); }, ); } }