diff --git a/lib/features/tasks/presentation/pages/calendar_page.dart b/lib/features/tasks/presentation/pages/calendar_page.dart index 4f982c9..8b9cf33 100644 --- a/lib/features/tasks/presentation/pages/calendar_page.dart +++ b/lib/features/tasks/presentation/pages/calendar_page.dart @@ -2,7 +2,6 @@ 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'; @@ -35,7 +34,6 @@ class _CalendarPageState extends State { 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++) { @@ -60,19 +58,24 @@ class _CalendarPageState extends State { } } - Priority? _getHighestPriority(List? tasks) { - if (tasks == null || tasks.isEmpty) return null; + List _getUniquePriorities(List? tasks) { + if (tasks == null || tasks.isEmpty) return []; final incompleteTasks = tasks.where((t) => !t.isDone).toList(); - if (incompleteTasks.isEmpty) return null; + if (incompleteTasks.isEmpty) return []; + // Get unique priorities, sorted by importance (high first) + final priorities = []; if (incompleteTasks.any((t) => t.priority == Priority.high)) { - return Priority.high; + priorities.add(Priority.high); } if (incompleteTasks.any((t) => t.priority == Priority.medium)) { - return Priority.medium; + priorities.add(Priority.medium); } - return Priority.low; + if (incompleteTasks.any((t) => t.priority == Priority.low)) { + priorities.add(Priority.low); + } + return priorities; } @override @@ -114,7 +117,7 @@ class _CalendarPageState extends State { focusedMonth: _focusedMonth, selectedDate: _selectedDate, tasksByDate: _tasksByDate, - getHighestPriority: _getHighestPriority, + getUniquePriorities: _getUniquePriorities, onDateSelected: (date) { setState(() => _selectedDate = date); }, @@ -126,12 +129,15 @@ class _CalendarPageState extends State { ? SafeArea( child: Padding( padding: const EdgeInsets.all(16), - child: ElevatedButton( - onPressed: () { - final dateStr = DateFormat('yyyy-MM-dd').format(_selectedDate!); - context.go('/?date=$dateStr'); - }, - child: Text(l10n.goToDay), + child: SizedBox( + height: 56, + child: ElevatedButton( + onPressed: () { + final dateStr = DateFormat('yyyy-MM-dd').format(_selectedDate!); + context.go('/?date=$dateStr'); + }, + child: Text(l10n.goToDay), + ), ), ), ) @@ -158,21 +164,29 @@ class _MonthNavigation extends StatelessWidget { final monthFormat = DateFormat.yMMMM(locale); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - IconButton( - icon: const Icon(Icons.chevron_left), - onPressed: onPrevious, + 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, ), - IconButton( - icon: const Icon(Icons.chevron_right), - onPressed: onNext, + SizedBox( + width: 48, + height: 48, + child: IconButton( + icon: const Icon(Icons.chevron_right, size: 28), + onPressed: onNext, + ), ), ], ), @@ -216,14 +230,14 @@ class _CalendarGrid extends StatelessWidget { final DateTime focusedMonth; final DateTime? selectedDate; final Map> tasksByDate; - final Priority? Function(List?) getHighestPriority; + final List Function(List?) getUniquePriorities; final ValueChanged onDateSelected; const _CalendarGrid({ required this.focusedMonth, required this.selectedDate, required this.tasksByDate, - required this.getHighestPriority, + required this.getUniquePriorities, required this.onDateSelected, }); @@ -237,10 +251,12 @@ class _CalendarGrid extends StatelessWidget { final today = DateTime.now(); return GridView.builder( - padding: const EdgeInsets.all(8), + 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) { @@ -260,12 +276,11 @@ class _CalendarGrid extends StatelessWidget { date.day == selectedDate!.day; final tasks = tasksByDate[dateStr]; - final highestPriority = getHighestPriority(tasks); + final priorities = getUniquePriorities(tasks); return GestureDetector( onTap: () => onDateSelected(date), child: Container( - margin: const EdgeInsets.all(2), decoration: BoxDecoration( color: isSelected ? Theme.of(context).colorScheme.primary @@ -288,10 +303,10 @@ class _CalendarGrid extends StatelessWidget { fontWeight: isToday || isSelected ? FontWeight.bold : null, ), ), - if (highestPriority != null) + if (priorities.isNotEmpty) Positioned( - bottom: 4, - child: _PulsingDot(priority: highestPriority), + bottom: 6, + child: _PriorityDots(priorities: priorities), ), ], ), @@ -302,6 +317,25 @@ class _CalendarGrid extends StatelessWidget { } } +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; @@ -354,15 +388,15 @@ class _PulsingDotState extends State<_PulsingDot> return Opacity( opacity: _animation.value, child: Container( - width: 6, - height: 6, + width: 10, + height: 10, decoration: BoxDecoration( color: _getPriorityColor(), shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: _getPriorityColor().withOpacity(0.5), - blurRadius: 2, + color: _getPriorityColor().withValues(alpha: 0.5), + blurRadius: 3, 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 2ff3200..fc67f5b 100644 --- a/lib/features/tasks/presentation/pages/daily_agenda_page.dart +++ b/lib/features/tasks/presentation/pages/daily_agenda_page.dart @@ -79,9 +79,12 @@ class _DailyAgendaView extends StatelessWidget { ], ), floatingActionButton: FloatingActionButton( - onPressed: () { + onPressed: () async { final dateStr = DateFormat('yyyy-MM-dd').format(vm.selectedDate); - context.push('/task/new?date=$dateStr'); + final result = await context.push('/task/new?date=$dateStr'); + if (result == true) { + vm.loadTasks(); + } }, child: const Icon(Icons.add), ), @@ -140,7 +143,12 @@ class _DailyAgendaView extends StatelessWidget { return TaskTile( task: task, onToggle: () => vm.toggleTask(task.id), - onTap: () => context.push('/task/${task.id}/edit'), + onTap: () async { + final result = await context.push('/task/${task.id}/edit'); + if (result == true) { + vm.loadTasks(); + } + }, onDelete: () => vm.deleteTask(task.id), onReschedule: () => vm.rescheduleToTomorrow(task.id), ); @@ -169,13 +177,17 @@ class _DateNavigation extends StatelessWidget { final dateFormat = DateFormat.yMMMMEEEEd(locale); return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - IconButton( - icon: const Icon(Icons.chevron_left), - onPressed: onPrevious, + SizedBox( + width: 48, + height: 48, + child: IconButton( + icon: const Icon(Icons.chevron_left, size: 28), + onPressed: onPrevious, + ), ), Expanded( child: Text( @@ -184,9 +196,13 @@ class _DateNavigation extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium, ), ), - IconButton( - icon: const Icon(Icons.chevron_right), - onPressed: onNext, + SizedBox( + width: 48, + height: 48, + child: IconButton( + icon: const Icon(Icons.chevron_right, size: 28), + onPressed: onNext, + ), ), ], ), diff --git a/lib/features/tasks/presentation/pages/task_form_page.dart b/lib/features/tasks/presentation/pages/task_form_page.dart index e066dd8..4e595d7 100644 --- a/lib/features/tasks/presentation/pages/task_form_page.dart +++ b/lib/features/tasks/presentation/pages/task_form_page.dart @@ -99,52 +99,59 @@ class _TaskFormView extends StatelessWidget { onChanged: vm.setDescription, maxLines: 3, ), - const SizedBox(height: 16), - ListTile( - title: Text(l10n.date), - subtitle: Text(DateFormat.yMMMd(locale).format(vm.date)), - trailing: const Icon(Icons.calendar_today), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide(color: Theme.of(context).colorScheme.outline), + const SizedBox(height: 20), + ConstrainedBox( + constraints: const BoxConstraints(minHeight: 72), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + title: Text(l10n.date), + subtitle: Text(DateFormat.yMMMd(locale).format(vm.date)), + trailing: const Icon(Icons.calendar_today, size: 24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Theme.of(context).colorScheme.outline), + ), + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: vm.date, + firstDate: DateTime(2020), + lastDate: DateTime(2100), + ); + if (picked != null) { + vm.setDate(picked); + } + }, ), - onTap: () async { - final picked = await showDatePicker( - context: context, - initialDate: vm.date, - firstDate: DateTime(2020), - lastDate: DateTime(2100), - ); - if (picked != null) { - vm.setDate(picked); - } - }, ), - const SizedBox(height: 16), + const SizedBox(height: 20), Text( l10n.priority, style: Theme.of(context).textTheme.titleSmall, ), - const SizedBox(height: 8), - SegmentedButton( - segments: [ - ButtonSegment( - value: Priority.low, - label: Text(l10n.priorityLow), - ), - ButtonSegment( - value: Priority.medium, - label: Text(l10n.priorityMedium), - ), - ButtonSegment( - value: Priority.high, - label: Text(l10n.priorityHigh), - ), - ], - selected: {vm.priority}, - onSelectionChanged: (selection) { - vm.setPriority(selection.first); - }, + const SizedBox(height: 12), + SizedBox( + height: 48, + child: SegmentedButton( + segments: [ + ButtonSegment( + value: Priority.low, + label: Text(l10n.priorityLow), + ), + ButtonSegment( + value: Priority.medium, + label: Text(l10n.priorityMedium), + ), + ButtonSegment( + value: Priority.high, + label: Text(l10n.priorityHigh), + ), + ], + selected: {vm.priority}, + onSelectionChanged: (selection) { + vm.setPriority(selection.first); + }, + ), ), if (vm.status == FormStatus.error && vm.failure != null) ...[ const SizedBox(height: 24), diff --git a/lib/features/tasks/presentation/viewmodels/daily_tasks_viewmodel.dart b/lib/features/tasks/presentation/viewmodels/daily_tasks_viewmodel.dart index f6d2989..d9a8765 100644 --- a/lib/features/tasks/presentation/viewmodels/daily_tasks_viewmodel.dart +++ b/lib/features/tasks/presentation/viewmodels/daily_tasks_viewmodel.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import '../../../../core/errors/failures.dart'; import '../../../../core/logging/app_logger.dart'; import '../../domain/entities/task_entity.dart'; +import '../../domain/enums/priority.dart'; import '../../domain/repositories/task_repository.dart'; enum TasksStatus { initial, loading, success, error } @@ -31,13 +32,31 @@ class DailyTasksViewModel extends ChangeNotifier { TaskFilter get filter => _filter; List get _filteredTasks { + List filtered; switch (_filter) { case TaskFilter.all: - return _tasks; + filtered = List.from(_tasks); + break; case TaskFilter.active: - return _tasks.where((t) => !t.isDone).toList(); + filtered = _tasks.where((t) => !t.isDone).toList(); + break; case TaskFilter.completed: - return _tasks.where((t) => t.isDone).toList(); + filtered = _tasks.where((t) => t.isDone).toList(); + break; + } + // Sort by priority: high first, then medium, then low + filtered.sort((a, b) => _priorityOrder(a.priority).compareTo(_priorityOrder(b.priority))); + return filtered; + } + + int _priorityOrder(Priority priority) { + switch (priority) { + case Priority.high: + return 0; + case Priority.medium: + return 1; + case Priority.low: + return 2; } } diff --git a/lib/features/tasks/presentation/widgets/filter_chips.dart b/lib/features/tasks/presentation/widgets/filter_chips.dart index f1d3e57..583847a 100644 --- a/lib/features/tasks/presentation/widgets/filter_chips.dart +++ b/lib/features/tasks/presentation/widgets/filter_chips.dart @@ -26,18 +26,21 @@ class FilterChips extends StatelessWidget { label: Text(l10n.filterAll), selected: currentFilter == TaskFilter.all, onSelected: (_) => onFilterChanged(TaskFilter.all), + materialTapTargetSize: MaterialTapTargetSize.padded, ), const SizedBox(width: 8), FilterChip( label: Text(l10n.filterActive), selected: currentFilter == TaskFilter.active, onSelected: (_) => onFilterChanged(TaskFilter.active), + materialTapTargetSize: MaterialTapTargetSize.padded, ), const SizedBox(width: 8), FilterChip( label: Text(l10n.filterCompleted), selected: currentFilter == TaskFilter.completed, onSelected: (_) => onFilterChanged(TaskFilter.completed), + materialTapTargetSize: MaterialTapTargetSize.padded, ), ], ), diff --git a/lib/features/tasks/presentation/widgets/task_tile.dart b/lib/features/tasks/presentation/widgets/task_tile.dart index 84c4618..42a6925 100644 --- a/lib/features/tasks/presentation/widgets/task_tile.dart +++ b/lib/features/tasks/presentation/widgets/task_tile.dart @@ -69,28 +69,33 @@ class TaskTile extends StatelessWidget { }, onDismissed: (_) => onDelete(), child: Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Container( - width: 4, - height: 48, - decoration: BoxDecoration( - color: priorityColor, - borderRadius: BorderRadius.circular(2), + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 72), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + Container( + width: 5, + height: 56, + decoration: BoxDecoration( + color: priorityColor, + borderRadius: BorderRadius.circular(3), + ), ), - ), - const SizedBox(width: 12), - Checkbox( - value: task.isDone, - onChanged: (_) => onToggle(), - ), - const SizedBox(width: 8), + SizedBox( + width: 48, + height: 48, + child: Checkbox( + value: task.isDone, + onChanged: (_) => onToggle(), + ), + ), + const SizedBox(width: 4), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -118,8 +123,12 @@ class TaskTile extends StatelessWidget { ], ), ), - PopupMenuButton( - onSelected: (value) async { + SizedBox( + width: 48, + height: 48, + child: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: (value) async { if (value == 'reschedule') { onReschedule(); } else if (value == 'delete') { @@ -145,33 +154,37 @@ class TaskTile extends StatelessWidget { } } }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'reschedule', - child: Row( - children: [ - const Icon(Icons.schedule), - const SizedBox(width: 8), - Text(l10n.rescheduleToTomorrow), - ], + itemBuilder: (context) => [ + PopupMenuItem( + height: 56, + value: 'reschedule', + child: Row( + children: [ + const Icon(Icons.schedule, size: 24), + const SizedBox(width: 12), + Text(l10n.rescheduleToTomorrow), + ], + ), ), - ), - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon(Icons.delete, color: Theme.of(context).colorScheme.error), - const SizedBox(width: 8), - Text(l10n.delete, style: TextStyle(color: Theme.of(context).colorScheme.error)), - ], + PopupMenuItem( + height: 56, + value: 'delete', + child: Row( + children: [ + Icon(Icons.delete, size: 24, color: Theme.of(context).colorScheme.error), + const SizedBox(width: 12), + Text(l10n.delete, style: TextStyle(color: Theme.of(context).colorScheme.error)), + ], + ), ), - ), - ], + ], + ), ), ], ), ), ), + ), ), ); }