From 6fb00fec5dcb9c76c62d72e1b2807b017c53e891 Mon Sep 17 00:00:00 2001 From: "kihong.kim" Date: Sat, 24 Jan 2026 23:45:22 +0900 Subject: [PATCH] Add todos admin and 30s refresh --- .../lib/screens/admin/admin_screen.dart | 229 ++++++++++- .../lib/widgets/announcement_widget.dart | 17 +- .../lib/widgets/bible_verse_widget.dart | 24 +- .../lib/widgets/schedule_list_widget.dart | 243 ++++++----- flutter_app/lib/widgets/todo_list_widget.dart | 380 ++++++++++-------- 5 files changed, 617 insertions(+), 276 deletions(-) diff --git a/flutter_app/lib/screens/admin/admin_screen.dart b/flutter_app/lib/screens/admin/admin_screen.dart index 0b824e0..bf5d7d0 100644 --- a/flutter_app/lib/screens/admin/admin_screen.dart +++ b/flutter_app/lib/screens/admin/admin_screen.dart @@ -13,6 +13,8 @@ import '../../services/family_service.dart'; import '../../services/photo_service.dart'; import '../../services/announcement_service.dart'; import '../../services/schedule_service.dart'; +import '../../models/todo_item.dart'; +import '../../services/todo_service.dart'; class AdminScreen extends StatefulWidget { const AdminScreen({super.key}); @@ -25,7 +27,7 @@ class _AdminScreenState extends State { @override Widget build(BuildContext context) { return DefaultTabController( - length: 5, + length: 6, child: Scaffold( appBar: AppBar( title: const Text('Admin Settings'), @@ -37,6 +39,7 @@ class _AdminScreenState extends State { Tab(text: 'Bible Verses'), Tab(text: 'Announcements'), Tab(text: 'Schedules'), + Tab(text: 'Todos'), ], ), ), @@ -47,6 +50,7 @@ class _AdminScreenState extends State { BibleVerseManagerTab(), AnnouncementManagerTab(), ScheduleManagerTab(), + TodoManagerTab(), ], ), ), @@ -1239,3 +1243,226 @@ class _ScheduleManagerTabState extends State { ); } } + +class TodoManagerTab extends StatefulWidget { + const TodoManagerTab({super.key}); + + @override + State createState() => _TodoManagerTabState(); +} + +class _TodoManagerTabState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () => _showAddTodoDialog(context), + child: const Icon(Icons.check_box), + ), + body: FutureBuilder>( + future: Provider.of(context).fetchTodos(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + final todos = snapshot.data!; + if (todos.isEmpty) { + return const Center( + child: Text( + 'No todos added yet', + style: TextStyle(color: Colors.grey), + ), + ); + } + return ListView.builder( + itemCount: todos.length, + itemBuilder: (context, index) { + final todo = todos[index]; + return ListTile( + leading: Checkbox( + value: todo.completed, + onChanged: (val) async { + await Provider.of( + context, + listen: false, + ).updateTodo( + TodoItem( + id: todo.id, + familyMemberId: todo.familyMemberId, + title: todo.title, + completed: val ?? false, + dueDate: todo.dueDate, + ), + ); + setState(() {}); + }, + ), + title: Text( + todo.title, + style: TextStyle( + decoration: + todo.completed ? TextDecoration.lineThrough : null, + ), + ), + subtitle: todo.dueDate != null + ? Text( + todo.dueDate!.toIso8601String().split('T')[0], + style: + const TextStyle(fontSize: 12, color: Colors.grey), + ) + : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit, color: Colors.blue), + onPressed: () => _showEditTodoDialog(context, todo), + ), + IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () async { + await Provider.of( + context, + listen: false, + ).deleteTodo(todo.id); + setState(() {}); + }, + ), + ], + ), + ); + }, + ); + }, + ), + ); + } + + void _showAddTodoDialog(BuildContext context) { + _showTodoDialog(context, null); + } + + void _showEditTodoDialog(BuildContext context, TodoItem todo) { + _showTodoDialog(context, todo); + } + + void _showTodoDialog(BuildContext context, TodoItem? todo) { + final isEditing = todo != null; + final titleController = TextEditingController(text: todo?.title ?? ''); + final dateController = TextEditingController( + text: todo?.dueDate?.toIso8601String().split('T')[0] ?? '', + ); + + String? selectedFamilyMemberId = todo?.familyMemberId; + bool isCompleted = todo?.completed ?? false; + + showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text(isEditing ? 'Edit Todo' : 'Add Todo'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: titleController, + decoration: const InputDecoration(labelText: 'Title'), + ), + TextField( + controller: dateController, + decoration: const InputDecoration( + labelText: 'Due Date (YYYY-MM-DD)', + hintText: '2024-01-01', + ), + ), + FutureBuilder>( + future: Provider.of(context, listen: false) + .fetchFamilyMembers(), + builder: (context, snapshot) { + if (!snapshot.hasData) return const SizedBox(); + final members = snapshot.data!; + return DropdownButtonFormField( + value: selectedFamilyMemberId, + decoration: const InputDecoration( + labelText: 'Family Member', + ), + items: members.map((member) { + return DropdownMenuItem( + value: member.id, + child: Text(member.name), + ); + }).toList(), + onChanged: (value) { + setDialogState(() { + selectedFamilyMemberId = value; + }); + }, + ); + }, + ), + if (isEditing) + SwitchListTile( + title: const Text('Completed'), + value: isCompleted, + onChanged: (value) { + setDialogState(() { + isCompleted = value; + }); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + if (titleController.text.isNotEmpty && + selectedFamilyMemberId != null) { + final date = dateController.text.isNotEmpty + ? DateTime.tryParse(dateController.text) + : null; + + final newItem = TodoItem( + id: todo?.id ?? '', + familyMemberId: selectedFamilyMemberId!, + title: titleController.text, + completed: isCompleted, + dueDate: date, + ); + + if (isEditing) { + await Provider.of( + context, + listen: false, + ).updateTodo(newItem); + } else { + await Provider.of( + context, + listen: false, + ).createTodo(newItem); + } + + if (mounted) { + Navigator.pop(context); + setState(() {}); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Title and Family Member are required')), + ); + } + }, + child: Text(isEditing ? 'Save' : 'Add'), + ), + ], + ), + ), + ); + } +} diff --git a/flutter_app/lib/widgets/announcement_widget.dart b/flutter_app/lib/widgets/announcement_widget.dart index ffa059c..f155b74 100644 --- a/flutter_app/lib/widgets/announcement_widget.dart +++ b/flutter_app/lib/widgets/announcement_widget.dart @@ -13,7 +13,8 @@ class AnnouncementWidget extends StatefulWidget { class _AnnouncementWidgetState extends State { final PageController _pageController = PageController(); - Timer? _timer; + Timer? _scrollTimer; + Timer? _refreshTimer; int _currentPage = 0; List _announcements = []; @@ -21,6 +22,13 @@ class _AnnouncementWidgetState extends State { void initState() { super.initState(); _fetchAnnouncements(); + _startAutoRefresh(); + } + + void _startAutoRefresh() { + _refreshTimer = Timer.periodic(const Duration(seconds: 30), (timer) { + _fetchAnnouncements(); + }); } void _fetchAnnouncements() async { @@ -41,9 +49,9 @@ class _AnnouncementWidgetState extends State { } void _startAutoScroll() { - _timer?.cancel(); + _scrollTimer?.cancel(); if (_announcements.length > 1) { - _timer = Timer.periodic(const Duration(seconds: 10), (timer) { + _scrollTimer = Timer.periodic(const Duration(seconds: 10), (timer) { if (_pageController.hasClients) { _currentPage++; if (_currentPage >= _announcements.length) { @@ -61,7 +69,8 @@ class _AnnouncementWidgetState extends State { @override void dispose() { - _timer?.cancel(); + _scrollTimer?.cancel(); + _refreshTimer?.cancel(); _pageController.dispose(); super.dispose(); } diff --git a/flutter_app/lib/widgets/bible_verse_widget.dart b/flutter_app/lib/widgets/bible_verse_widget.dart index 75114b4..255760d 100644 --- a/flutter_app/lib/widgets/bible_verse_widget.dart +++ b/flutter_app/lib/widgets/bible_verse_widget.dart @@ -3,9 +3,26 @@ import 'package:provider/provider.dart'; import '../models/bible_verse.dart'; import '../services/bible_service.dart'; -class BibleVerseWidget extends StatelessWidget { +class BibleVerseWidget extends StatefulWidget { const BibleVerseWidget({super.key}); + @override + State createState() => _BibleVerseWidgetState(); +} + +class _BibleVerseWidgetState extends State { + late Future _verseFuture; + + @override + void initState() { + super.initState(); + // Fetch only once on init + _verseFuture = Provider.of( + context, + listen: false, + ).fetchTodayVerse(); + } + @override Widget build(BuildContext context) { return Container( @@ -24,10 +41,7 @@ class BibleVerseWidget extends StatelessWidget { ), padding: const EdgeInsets.all(24), child: FutureBuilder( - future: Provider.of( - context, - listen: false, - ).fetchTodayVerse(), + future: _verseFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); diff --git a/flutter_app/lib/widgets/schedule_list_widget.dart b/flutter_app/lib/widgets/schedule_list_widget.dart index a70e4e9..0faf1a3 100644 --- a/flutter_app/lib/widgets/schedule_list_widget.dart +++ b/flutter_app/lib/widgets/schedule_list_widget.dart @@ -1,12 +1,66 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; import '../models/schedule_item.dart'; import '../services/schedule_service.dart'; -class ScheduleListWidget extends StatelessWidget { +class ScheduleListWidget extends StatefulWidget { const ScheduleListWidget({super.key}); + @override + State createState() => _ScheduleListWidgetState(); +} + +class _ScheduleListWidgetState extends State { + Timer? _timer; + List _schedules = []; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _fetchSchedules(); + _startAutoRefresh(); + } + + void _startAutoRefresh() { + _timer = Timer.periodic(const Duration(seconds: 30), (timer) { + _fetchSchedules(); + }); + } + + Future _fetchSchedules() async { + try { + final data = await Provider.of( + context, + listen: false, + ).fetchWeeklySchedules(); + + if (mounted) { + setState(() { + _schedules = data; + _isLoading = false; + _error = null; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _error = 'Failed to load schedules'; + _isLoading = false; + }); + } + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Container( @@ -21,109 +75,102 @@ class ScheduleListWidget extends StatelessWidget { Text( 'Weekly Schedule', style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ), + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 12), Expanded( - child: FutureBuilder>( - future: Provider.of( - context, - listen: false, - ).fetchWeeklySchedules(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (snapshot.hasError) { - return const Center( - child: Text( - 'Failed to load schedules', - style: TextStyle(color: Colors.white54), - ), - ); - } - - if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const Center( - child: Text( - 'No schedules this week', - style: TextStyle(color: Colors.white54), - ), - ); - } - - final schedules = snapshot.data!; - // Sort by date - schedules.sort((a, b) => a.startDate.compareTo(b.startDate)); - - return ListView.separated( - itemCount: schedules.length, - separatorBuilder: (context, index) => - const Divider(color: Colors.white10), - itemBuilder: (context, index) { - final item = schedules[index]; - final dateStr = DateFormat( - 'E, MMM d', - ).format(item.startDate); - final timeStr = item.isAllDay - ? 'All Day' - : DateFormat('HH:mm').format(item.startDate); - - return ListTile( - contentPadding: EdgeInsets.zero, - leading: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.white10, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - DateFormat('d').format(item.startDate), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - Text( - DateFormat('E').format(item.startDate), - style: const TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ], - ), - ), - title: Text( - item.title, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - timeStr, - style: const TextStyle(color: Colors.white54), - ), - ); - }, - ); - }, - ), + child: _buildContent(), ), ], ), ); } + + Widget _buildContent() { + if (_isLoading && _schedules.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (_error != null && _schedules.isEmpty) { + return Center( + child: Text( + _error!, + style: const TextStyle(color: Colors.white54), + ), + ); + } + + if (_schedules.isEmpty) { + return const Center( + child: Text( + 'No schedules this week', + style: TextStyle(color: Colors.white54), + ), + ); + } + + // Sort by date + final sortedSchedules = List.from(_schedules); + sortedSchedules.sort((a, b) => a.startDate.compareTo(b.startDate)); + + return ListView.separated( + itemCount: sortedSchedules.length, + separatorBuilder: (context, index) => + const Divider(color: Colors.white10), + itemBuilder: (context, index) { + final item = sortedSchedules[index]; + final timeStr = item.isAllDay + ? 'All Day' + : DateFormat('HH:mm').format(item.startDate); + + return ListTile( + contentPadding: EdgeInsets.zero, + leading: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + DateFormat('d').format(item.startDate), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + Text( + DateFormat('E').format(item.startDate), + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + title: Text( + item.title, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + timeStr, + style: const TextStyle(color: Colors.white54), + ), + ); + }, + ); + } } diff --git a/flutter_app/lib/widgets/todo_list_widget.dart b/flutter_app/lib/widgets/todo_list_widget.dart index 2be0e8f..0f35a96 100644 --- a/flutter_app/lib/widgets/todo_list_widget.dart +++ b/flutter_app/lib/widgets/todo_list_widget.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/todo_item.dart'; @@ -5,9 +6,70 @@ import '../models/family_member.dart'; import '../services/todo_service.dart'; import '../services/family_service.dart'; -class TodoListWidget extends StatelessWidget { +class TodoListWidget extends StatefulWidget { const TodoListWidget({super.key}); + @override + State createState() => _TodoListWidgetState(); +} + +class _TodoListWidgetState extends State { + Timer? _timer; + List _todos = []; + List _members = []; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _fetchData(); + _startAutoRefresh(); + } + + void _startAutoRefresh() { + _timer = Timer.periodic(const Duration(seconds: 30), (timer) { + _fetchData(); + }); + } + + Future _fetchData() async { + try { + final results = await Future.wait([ + Provider.of( + context, + listen: false, + ).fetchTodayTodos(), + Provider.of( + context, + listen: false, + ).fetchFamilyMembers(), + ]); + + if (mounted) { + setState(() { + _todos = results[0] as List; + _members = results[1] as List; + _isLoading = false; + _error = null; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _error = 'Failed to load todos'; + _isLoading = false; + }); + } + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Container( @@ -37,176 +99,158 @@ class TodoListWidget extends StatelessWidget { ), const SizedBox(height: 12), Expanded( - child: FutureBuilder>( - future: Future.wait([ - Provider.of( - context, - listen: false, - ).fetchTodayTodos(), - Provider.of( - context, - listen: false, - ).fetchFamilyMembers(), - ]), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (snapshot.hasError) { - return const Center( - child: Text( - 'Failed to load todos', - style: TextStyle(color: Colors.white54), - ), - ); - } - - if (!snapshot.hasData) { - return const Center( - child: Text( - 'No todos today', - style: TextStyle(color: Colors.white54), - ), - ); - } - - final todos = snapshot.data![0] as List; - final members = snapshot.data![1] as List; - - if (todos.isEmpty) { - return const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.thumb_up, color: Colors.white24, size: 32), - SizedBox(height: 8), - Text( - 'All done for today!', - style: TextStyle(color: Colors.white54), - ), - ], - ), - ); - } - - return ListView.separated( - itemCount: todos.length, - separatorBuilder: (context, index) => - const Divider(color: Colors.white10), - itemBuilder: (context, index) { - final todo = todos[index]; - final member = members.firstWhere( - (m) => m.id == todo.familyMemberId, - orElse: () => const FamilyMember( - id: '', - name: 'Unknown', - iconUrl: '', - emoji: '👤', - color: '#888888', - order: 0, - ), - ); - - // Parse color - Color memberColor; - try { - String cleanHex = - member.color.replaceAll('#', '').replaceAll('0x', ''); - if (cleanHex.length == 6) { - cleanHex = 'FF$cleanHex'; - } - if (cleanHex.length == 8) { - memberColor = Color(int.parse('0x$cleanHex')); - } else { - memberColor = Colors.grey; - } - } catch (_) { - memberColor = Colors.grey; - } - - return ListTile( - contentPadding: EdgeInsets.zero, - leading: CircleAvatar( - backgroundColor: memberColor.withOpacity(0.2), - child: member.iconUrl.isNotEmpty - ? ClipOval( - child: Image.network( - member.iconUrl, - width: 40, - height: 40, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Text( - member.name.isNotEmpty - ? member.name[0].toUpperCase() - : '?', - style: TextStyle( - color: memberColor, - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ); - }, - ), - ) - : (member.name.isNotEmpty - ? Text( - member.name[0].toUpperCase(), - style: TextStyle( - color: memberColor, - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ) - : Icon( - Icons.person, - color: memberColor, - )), - ), - title: Text( - todo.title, - style: TextStyle( - color: todo.completed ? Colors.white54 : Colors.white, - decoration: todo.completed - ? TextDecoration.lineThrough - : null, - decorationColor: Colors.white54, - ), - ), - trailing: Checkbox( - value: todo.completed, - onChanged: (val) async { - // Toggle completion - final updated = TodoItem( - id: todo.id, - familyMemberId: todo.familyMemberId, - title: todo.title, - completed: val ?? false, - dueDate: todo.dueDate, - ); - await Provider.of( - context, - listen: false, - ).updateTodo(updated); - // Force rebuild? In a real app we'd use a reactive state. - // Here we rely on the parent or timer to refresh, or we could convert this to StatefulWidget. - // For now, let's just let the next refresh cycle pick it up, or if the user interacts, maybe we should optimistic update? - // Given it's a TV dashboard, interaction might be rare, but if it is interactive: - (context as Element) - .markNeedsBuild(); // HACK to refresh - }, - activeColor: Theme.of(context).colorScheme.secondary, - checkColor: Colors.black, - ), - ); - }, - ); - }, - ), + child: _buildContent(), ), ], ), ); } + + Widget _buildContent() { + if (_isLoading && _todos.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (_error != null && _todos.isEmpty) { + return Center( + child: Text( + _error!, + style: const TextStyle(color: Colors.white54), + ), + ); + } + + if (_todos.isEmpty) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.thumb_up, color: Colors.white24, size: 32), + SizedBox(height: 8), + Text( + 'All done for today!', + style: TextStyle(color: Colors.white54), + ), + ], + ), + ); + } + + return ListView.separated( + itemCount: _todos.length, + separatorBuilder: (context, index) => + const Divider(color: Colors.white10), + itemBuilder: (context, index) { + final todo = _todos[index]; + final member = _members.firstWhere( + (m) => m.id == todo.familyMemberId, + orElse: () => const FamilyMember( + id: '', + name: 'Unknown', + iconUrl: '', + emoji: '👤', + color: '#888888', + order: 0, + ), + ); + + // Parse color + Color memberColor; + try { + String cleanHex = + member.color.replaceAll('#', '').replaceAll('0x', ''); + if (cleanHex.length == 6) { + cleanHex = 'FF$cleanHex'; + } + if (cleanHex.length == 8) { + memberColor = Color(int.parse('0x$cleanHex')); + } else { + memberColor = Colors.grey; + } + } catch (_) { + memberColor = Colors.grey; + } + + return ListTile( + contentPadding: EdgeInsets.zero, + leading: CircleAvatar( + backgroundColor: memberColor.withOpacity(0.2), + child: member.iconUrl.isNotEmpty + ? ClipOval( + child: Image.network( + member.iconUrl, + width: 40, + height: 40, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Text( + member.name.isNotEmpty + ? member.name[0].toUpperCase() + : '?', + style: TextStyle( + color: memberColor, + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ); + }, + ), + ) + : (member.name.isNotEmpty + ? Text( + member.name[0].toUpperCase(), + style: TextStyle( + color: memberColor, + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ) + : Icon( + Icons.person, + color: memberColor, + )), + ), + title: Text( + todo.title, + style: TextStyle( + color: todo.completed ? Colors.white54 : Colors.white, + decoration: todo.completed ? TextDecoration.lineThrough : null, + decorationColor: Colors.white54, + ), + ), + trailing: Checkbox( + value: todo.completed, + onChanged: (val) async { + // Toggle completion + final updated = TodoItem( + id: todo.id, + familyMemberId: todo.familyMemberId, + title: todo.title, + completed: val ?? false, + dueDate: todo.dueDate, + ); + + // Optimistic update + setState(() { + final idx = _todos.indexWhere((t) => t.id == todo.id); + if (idx != -1) { + _todos[idx] = updated; + } + }); + + await Provider.of( + context, + listen: false, + ).updateTodo(updated); + + // Refresh to ensure sync + _fetchData(); + }, + activeColor: Theme.of(context).colorScheme.secondary, + checkColor: Colors.black, + ), + ); + }, + ); + } }