diff --git a/lib/main.dart b/lib/main.dart index d1a3076..5a015e7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -98,11 +98,11 @@ class MyApp extends StatelessWidget { title: 'Supplements Tracker', theme: ShadThemeData( brightness: Brightness.light, - colorScheme: const ShadBlueColorScheme.light(), + colorScheme: const ShadZincColorScheme.light(), ), darkTheme: ShadThemeData( brightness: Brightness.dark, - colorScheme: const ShadBlueColorScheme.dark(), + colorScheme: const ShadZincColorScheme.dark(), ), themeMode: settingsProvider.themeMode, home: const HomeScreen(), diff --git a/lib/providers/supplement_provider.dart b/lib/providers/supplement_provider.dart index 43df790..902060b 100644 --- a/lib/providers/supplement_provider.dart +++ b/lib/providers/supplement_provider.dart @@ -367,6 +367,24 @@ class SupplementProvider with ChangeNotifier, WidgetsBindingObserver { return _todayIntakes.where((intake) => intake['supplement_id'] == supplementId).length; } + /// Get the most recent intake for a supplement today + Map? getMostRecentIntake(int supplementId) { + final supplementIntakes = _todayIntakes + .where((intake) => intake['supplement_id'] == supplementId) + .toList(); + + if (supplementIntakes.isEmpty) return null; + + // Sort by takenAt time (most recent first) + supplementIntakes.sort((a, b) { + final aTime = DateTime.parse(a['takenAt']); + final bTime = DateTime.parse(b['takenAt']); + return bTime.compareTo(aTime); // Descending order + }); + + return supplementIntakes.first; + } + Map get dailyIngredientIntake { final Map ingredientIntake = {}; diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 144d0ec..881061c 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -6,6 +6,7 @@ import 'add_supplement_screen.dart'; import 'history_screen.dart'; import 'settings_screen.dart'; import 'supplements_list_screen.dart'; +import 'today_schedule_screen.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -18,6 +19,7 @@ class _HomeScreenState extends State { int _currentIndex = 0; final List _screens = [ + const TodayScheduleScreen(), const SupplementsListScreen(), const HistoryScreen(), const SettingsScreen(), @@ -45,11 +47,18 @@ class _HomeScreenState extends State { }); }, type: BottomNavigationBarType.fixed, + selectedItemColor: Theme.of(context).colorScheme.primary, + unselectedItemColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4), + backgroundColor: Theme.of(context).colorScheme.surface, items: const [ BottomNavigationBarItem( - icon: Icon(Icons.medication), + icon: Icon(Icons.medication_liquid_sharp), label: 'Supplements', ), + BottomNavigationBarItem( + icon: Icon(Icons.edit_calendar_rounded), + label: 'Edit', + ), BottomNavigationBarItem( icon: Icon(Icons.history), label: 'History', @@ -60,7 +69,7 @@ class _HomeScreenState extends State { ), ], ), - floatingActionButton: _currentIndex == 0 + floatingActionButton: _currentIndex == 1 ? FloatingActionButton( onPressed: () async { await Navigator.of(context).push( diff --git a/lib/screens/supplements_list_screen.dart b/lib/screens/supplements_list_screen.dart index 2789c6c..d7b9f1e 100644 --- a/lib/screens/supplements_list_screen.dart +++ b/lib/screens/supplements_list_screen.dart @@ -18,7 +18,7 @@ class SupplementsListScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('My Supplements'), + title: const Text('Edit Supplements'), backgroundColor: Theme.of(context).colorScheme.inversePrimary, actions: [ Consumer( @@ -100,18 +100,18 @@ class SupplementsListScreen extends StatelessWidget { await provider.loadSupplements(); await provider.refreshDailyStatus(); }, - child: _buildGroupedSupplementsList(context, provider.supplements, settingsProvider), + child: _buildGroupedSupplementsList(context, provider.supplements, settingsProvider, provider), ); }, ), ); } - Widget _buildGroupedSupplementsList(BuildContext context, List supplements, SettingsProvider settingsProvider) { + Widget _buildGroupedSupplementsList(BuildContext context, List supplements, SettingsProvider settingsProvider, SupplementProvider provider) { final groupedSupplements = _groupSupplementsByTimeOfDay(supplements, settingsProvider); return ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 80), children: [ if (groupedSupplements['morning']!.isNotEmpty) ...[ _buildSectionHeader('Morning (${settingsProvider.morningRange})', Icons.wb_sunny, Colors.orange, groupedSupplements['morning']!.length), @@ -123,6 +123,7 @@ class SupplementsListScreen extends StatelessWidget { onDelete: () => _deleteSupplement(context, supplement), onArchive: () => _archiveSupplement(context, supplement), onDuplicate: () => context.read().duplicateSupplement(supplement.id!), + showCompletionStatus: false, ), ), const SizedBox(height: 16), @@ -138,6 +139,7 @@ class SupplementsListScreen extends StatelessWidget { onDelete: () => _deleteSupplement(context, supplement), onArchive: () => _archiveSupplement(context, supplement), onDuplicate: () => context.read().duplicateSupplement(supplement.id!), + showCompletionStatus: false, ), ), const SizedBox(height: 16), @@ -153,6 +155,7 @@ class SupplementsListScreen extends StatelessWidget { onDelete: () => _deleteSupplement(context, supplement), onArchive: () => _archiveSupplement(context, supplement), onDuplicate: () => context.read().duplicateSupplement(supplement.id!), + showCompletionStatus: false, ), ), const SizedBox(height: 16), @@ -168,6 +171,7 @@ class SupplementsListScreen extends StatelessWidget { onDelete: () => _deleteSupplement(context, supplement), onArchive: () => _archiveSupplement(context, supplement), onDuplicate: () => context.read().duplicateSupplement(supplement.id!), + showCompletionStatus: false, ), ), const SizedBox(height: 16), @@ -183,9 +187,11 @@ class SupplementsListScreen extends StatelessWidget { onDelete: () => _deleteSupplement(context, supplement), onArchive: () => _archiveSupplement(context, supplement), onDuplicate: () => context.read().duplicateSupplement(supplement.id!), + showCompletionStatus: false, ), ), ], + ], ); } @@ -278,7 +284,7 @@ class SupplementsListScreen extends StatelessWidget { return grouped; } - + void _editSupplement(BuildContext context, Supplement supplement) { Navigator.of(context).push( MaterialPageRoute( diff --git a/lib/screens/today_schedule_screen.dart b/lib/screens/today_schedule_screen.dart new file mode 100644 index 0000000..90c119c --- /dev/null +++ b/lib/screens/today_schedule_screen.dart @@ -0,0 +1,456 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:popover/popover.dart'; + +import '../models/supplement.dart'; +import '../providers/settings_provider.dart'; +import '../providers/simple_sync_provider.dart'; +import '../providers/supplement_provider.dart'; +import '../services/database_sync_service.dart'; +import '../widgets/dialogs/take_supplement_dialog.dart'; + +class TodayScheduleScreen extends StatelessWidget { + const TodayScheduleScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("My Supplements"), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + actions: [ + Consumer( + builder: (context, syncProvider, child) { + if (!syncProvider.isConfigured) { + return const SizedBox.shrink(); + } + + return IconButton( + icon: syncProvider.isSyncing + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : syncProvider.status == SyncStatus.completed && + syncProvider.lastSyncTime != null && + DateTime.now().difference(syncProvider.lastSyncTime!).inSeconds < 5 + ? const Icon(Icons.check, color: Colors.green) + : const Icon(Icons.sync), + onPressed: syncProvider.isSyncing ? null : () { + syncProvider.syncDatabase(); + }, + tooltip: syncProvider.isSyncing ? 'Syncing...' : 'Force Sync', + ); + }, + ), + ], + ), + body: Consumer2( + builder: (context, provider, settingsProvider, child) { + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (provider.todayIntakes.isEmpty && provider.supplements.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.schedule, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + 'No schedule for today', + style: TextStyle( + fontSize: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Text( + 'Add supplements with reminder times to see your schedule', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } + + return RefreshIndicator( + onRefresh: () async { + await provider.loadSupplements(); + await provider.refreshDailyStatus(); + }, + child: _buildTimelineView(context, provider, settingsProvider), + ); + }, + ), + ); + } + + Widget _buildTimelineView(BuildContext context, SupplementProvider provider, SettingsProvider settingsProvider) { + final scheduledItems = _getScheduledItems(provider.supplements, provider.todayIntakes); + final groupedItems = _groupScheduledItemsByTimeOfDay(scheduledItems); + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + if (groupedItems['morning']!.isNotEmpty) ...[ + _buildTimeSectionHeader('Morning', Icons.wb_sunny, Colors.orange), + ...groupedItems['morning']!.map((item) => _buildScheduledItem(context, item)), + const SizedBox(height: 12), + ], + + if (groupedItems['afternoon']!.isNotEmpty) ...[ + _buildTimeSectionHeader('Afternoon', Icons.light_mode, Colors.blue), + ...groupedItems['afternoon']!.map((item) => _buildScheduledItem(context, item)), + const SizedBox(height: 12), + ], + + if (groupedItems['evening']!.isNotEmpty) ...[ + _buildTimeSectionHeader('Evening', Icons.nightlight_round, Colors.indigo), + ...groupedItems['evening']!.map((item) => _buildScheduledItem(context, item)), + const SizedBox(height: 12), + ], + + if (groupedItems['night']!.isNotEmpty) ...[ + _buildTimeSectionHeader('Night', Icons.bedtime, Colors.purple), + ...groupedItems['night']!.map((item) => _buildScheduledItem(context, item)), + ], + ], + ); + } + + Widget _buildTimeSectionHeader(String title, IconData icon, Color color) { + return Container( + margin: const EdgeInsets.only(bottom: 8, top: 8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ], + ), + ); + } + + Widget _buildScheduledItem(BuildContext context, Map item) { + final time = item['time'] as String; + final supplement = item['supplement'] as Supplement; + final status = item['status'] as String; + final actualTime = item['actualTime'] as String?; + + final isCompleted = status == 'on_time' || status == 'off_time'; + final isOnTime = status == 'on_time'; + + return Builder( + builder: (context) => InkWell( + onTap: () { + showPopover( + context: context, + bodyBuilder: (context) => Container( + constraints: const BoxConstraints(maxWidth: 150), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.shadow.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isCompleted) + _buildPopoverItem( + context: context, + icon: Icons.undo, + label: 'Undo Last Taken', + onTap: () async { + Navigator.of(context).pop(); + final provider = context.read(); + final mostRecentIntake = provider.getMostRecentIntake(supplement.id!); + if (mostRecentIntake != null) { + await provider.deleteIntake(mostRecentIntake['id']); + // Refresh the schedule after undoing + if (context.mounted) { + provider.refreshDailyStatus(); + } + } + }, + color: Colors.orange, + ), + _buildPopoverItem( + context: context, + icon: isCompleted ? Icons.add_circle_outline : Icons.medication, + label: isCompleted ? 'Take Again' : 'Take', + onTap: () async { + Navigator.of(context).pop(); + await showTakeSupplementDialog(context, supplement); + // Refresh the schedule after taking + if (context.mounted) { + context.read().refreshDailyStatus(); + } + }, + ), + ], + ), + ), + direction: PopoverDirection.top, + width: 180, + height: null, + arrowHeight: 0, + arrowWidth: 0, + backgroundColor: Colors.transparent, + shadow: const [], + ); + }, + borderRadius: BorderRadius.circular(8), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isCompleted + ? Theme.of(context).colorScheme.surface + : Theme.of(context).colorScheme.primary.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isCompleted + ? Theme.of(context).colorScheme.outline.withValues(alpha: 0.3) + : Theme.of(context).colorScheme.primary.withValues(alpha: 0.3), + ), + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + time, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: isCompleted + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.primary, + ), + ), + if (actualTime != null && actualTime != time) + Text( + 'Taken: $actualTime', + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.6), + ), + ), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Text( + supplement.name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isCompleted + ? Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface, + decoration: isCompleted ? TextDecoration.lineThrough : null, + ), + ), + ), + if (isCompleted) + Row( + children: [ + if (!isOnTime) + Icon( + Icons.schedule, + size: 14, + color: Colors.orange, + ), + const SizedBox(width: 4), + Icon( + Icons.check_circle, + size: 16, + color: isOnTime ? Colors.green : Colors.orange, + ), + ], + ) + else + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Pending', + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildPopoverItem({ + required BuildContext context, + required IconData icon, + required String label, + required VoidCallback onTap, + Color? color, + }) { + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + icon, + size: 18, + color: color ?? Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 12), + Text( + label, + style: TextStyle( + fontSize: 14, + color: color ?? Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + + List> _getScheduledItems(List supplements, List> todayIntakes) { + final List> scheduledItems = []; + + for (final supplement in supplements) { + for (final reminderTime in supplement.reminderTimes) { + // Parse reminder time (format: "HH:MM") + final parts = reminderTime.split(':'); + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1]); + + // Check if this reminder has been taken today + String status = 'pending'; + String? actualTime; + + for (final intake in todayIntakes) { + if (intake['supplement_id'] == supplement.id) { + final takenAt = DateTime.parse(intake['takenAt']); + final reminderDateTime = DateTime( + takenAt.year, + takenAt.month, + takenAt.day, + hour, + minute, + ); + + // Check if taken within 1 hour of reminder time + final timeDiff = (takenAt.difference(reminderDateTime)).inMinutes.abs(); + if (timeDiff <= 60) { + status = 'on_time'; + actualTime = '${takenAt.hour.toString().padLeft(2, '0')}:${takenAt.minute.toString().padLeft(2, '0')}'; + } else { + status = 'off_time'; + actualTime = '${takenAt.hour.toString().padLeft(2, '0')}:${takenAt.minute.toString().padLeft(2, '0')}'; + } + break; + } + } + + scheduledItems.add({ + 'time': reminderTime, + 'supplement': supplement, + 'status': status, + 'actualTime': actualTime, + }); + } + } + + // Sort by time + scheduledItems.sort((a, b) { + final timeA = a['time'] as String; + final timeB = b['time'] as String; + return timeA.compareTo(timeB); + }); + + return scheduledItems; + } + + Map>> _groupScheduledItemsByTimeOfDay(List> scheduledItems) { + final Map>> grouped = { + 'morning': >[], + 'afternoon': >[], + 'evening': >[], + 'night': >[], + }; + + for (final item in scheduledItems) { + final time = item['time'] as String; + final parts = time.split(':'); + final hour = int.parse(parts[0]); + + String category; + if (hour >= 6 && hour < 12) { + category = 'morning'; + } else if (hour >= 12 && hour < 18) { + category = 'afternoon'; + } else if (hour >= 18 && hour < 22) { + category = 'evening'; + } else { + category = 'night'; + } + + grouped[category]!.add(item); + } + + // Sort items by time within each category + for (final category in grouped.keys) { + grouped[category]!.sort((a, b) { + final timeA = a['time'] as String; + final timeB = b['time'] as String; + return timeA.compareTo(timeB); + }); + } + + return grouped; + } +} \ No newline at end of file diff --git a/lib/widgets/supplement_card.dart b/lib/widgets/supplement_card.dart index 37cf6f1..08e7286 100644 --- a/lib/widgets/supplement_card.dart +++ b/lib/widgets/supplement_card.dart @@ -1,6 +1,7 @@ -import 'package:flutter/material.dart'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; +import 'package:popover/popover.dart'; import '../models/supplement.dart'; import '../providers/supplement_provider.dart'; @@ -11,6 +12,8 @@ class SupplementCard extends StatefulWidget { final VoidCallback onDelete; final VoidCallback onArchive; final VoidCallback onDuplicate; + final VoidCallback? onUndoLastTaken; + final bool showCompletionStatus; const SupplementCard({ super.key, @@ -20,6 +23,8 @@ class SupplementCard extends StatefulWidget { required this.onDelete, required this.onArchive, required this.onDuplicate, + this.onUndoLastTaken, + this.showCompletionStatus = true, }); @override @@ -29,16 +34,48 @@ class SupplementCard extends StatefulWidget { class _SupplementCardState extends State { bool _isExpanded = false; + Widget _buildPopoverItem({ + required IconData icon, + required String label, + required VoidCallback onTap, + Color? color, + }) { + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + icon, + size: 18, + color: color ?? Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 12), + Text( + label, + style: TextStyle( + fontSize: 14, + color: color ?? Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return Consumer( builder: (context, provider, child) { - final bool isTakenToday = provider.hasBeenTakenToday(widget.supplement.id!); - final int todayIntakeCount = provider.getTodayIntakeCount(widget.supplement.id!); - final bool isCompletelyTaken = todayIntakeCount >= widget.supplement.frequencyPerDay; + final bool isTakenToday = widget.showCompletionStatus ? provider.hasBeenTakenToday(widget.supplement.id!) : false; + final int todayIntakeCount = widget.showCompletionStatus ? provider.getTodayIntakeCount(widget.supplement.id!) : 0; + final bool isCompletelyTaken = widget.showCompletionStatus ? todayIntakeCount >= widget.supplement.frequencyPerDay : false; - // Get today's intake times for this supplement - final todayIntakes = provider.todayIntakes + // Get today's intake times for this supplement (only if showing completion status) + final todayIntakes = widget.showCompletionStatus ? provider.todayIntakes .where((intake) => intake['supplement_id'] == widget.supplement.id) .map((intake) { final takenAt = DateTime.parse(intake['takenAt']); @@ -47,410 +84,227 @@ class _SupplementCardState extends State { 'time': '${takenAt.hour.toString().padLeft(2, '0')}:${takenAt.minute.toString().padLeft(2, '0')}', 'units': unitsTaken is int ? unitsTaken.toDouble() : unitsTaken as double, }; - }).toList(); + }).toList() : []; return Card( - margin: const EdgeInsets.only(bottom: 16), - elevation: 3, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: isCompletelyTaken - ? Theme.of(context).colorScheme.surface - : isTakenToday - ? Theme.of(context).colorScheme.secondaryContainer - : Theme.of(context).colorScheme.surface, - border: Border.all( - color: isCompletelyTaken - ? Colors.green.shade600 - : isTakenToday - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.outline.withValues(alpha: 0.2), - width: 1.5, - ), - ), - child: Theme( - data: Theme.of(context).copyWith( - dividerColor: Colors.transparent, - ), - child: ExpansionTile( - initiallyExpanded: _isExpanded, - onExpansionChanged: (expanded) { - setState(() { - _isExpanded = expanded; - }); - }, - tilePadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), - childrenPadding: const EdgeInsets.fromLTRB(20, 0, 20, 20), - leading: Container( - padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(bottom: 8), + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: InkWell( + onTap: () { + showPopover( + context: context, + bodyBuilder: (context) => Container( + constraints: const BoxConstraints(maxWidth: 200), decoration: BoxDecoration( - color: isCompletelyTaken - ? Colors.green.shade500 - : isTakenToday - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - shape: BoxShape.circle, - ), - child: Icon( - isCompletelyTaken - ? Icons.check_circle - : isTakenToday - ? Icons.schedule - : Icons.medication, - color: isCompletelyTaken - ? Theme.of(context).colorScheme.inverseSurface - : isTakenToday - ? Theme.of(context).colorScheme.onSecondary - : Theme.of(context).colorScheme.primary, - size: 20, - ), - ), - title: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.supplement.name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: isCompletelyTaken - ? Theme.of(context).colorScheme.inverseSurface - : isTakenToday - ? Theme.of(context).colorScheme.onSecondaryContainer - : Theme.of(context).colorScheme.onSurface, - ), - ), - if (widget.supplement.brand != null && widget.supplement.brand!.isNotEmpty) - Text( - widget.supplement.brand!, - style: TextStyle( - fontSize: 12, - color: isCompletelyTaken - ? Colors.green.shade200 - : isTakenToday - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500, - ), - ), - ], - ), + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), + width: 1, ), - // Status badge and take button in collapsed view - if (!_isExpanded) ...[ - if (isCompletelyTaken) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.green.shade500, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - 'Complete', - style: TextStyle( - color: Theme.of(context).colorScheme.inverseSurface, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ) - else ...[ - if (isTakenToday) - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - margin: const EdgeInsets.only(right: 8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondary, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - '$todayIntakeCount/${widget.supplement.frequencyPerDay}', - style: TextStyle( - color: Theme.of(context).colorScheme.onSecondary, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - ], + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.shadow.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), ], - ], - ), - trailing: PopupMenuButton( - padding: EdgeInsets.zero, - icon: Icon( - Icons.more_vert, - color: isCompletelyTaken - ? Theme.of(context).colorScheme.inverseSurface - : Theme.of(context).colorScheme.onSurfaceVariant, ), - onSelected: (value) { - switch (value) { - case 'edit': - widget.onEdit(); - break; - case 'duplicate': - widget.onDuplicate(); - break; - case 'archive': - widget.onArchive(); - break; - case 'delete': - widget.onDelete(); - break; - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'take', - onTap: widget.onTake, - child: Row( - children: [ - Icon(isTakenToday ? Icons.add_circle_outline : Icons.medication), - const SizedBox(width: 8), - Text(isTakenToday ? 'Take Again' : 'Take'), - ], - ), - ), - const PopupMenuItem( - value: 'edit', - child: Row( - children: [ - Icon(Icons.edit), - SizedBox(width: 8), - Text('Edit'), - ], - ), - ), - const PopupMenuItem( - value: 'duplicate', - child: Row( - children: [ - Icon(Icons.copy), - SizedBox(width: 8), - Text('Duplicate'), - ], - ), - ), - const PopupMenuItem( - value: 'archive', - child: Row( - children: [ - Icon(Icons.archive, color: Colors.orange), - SizedBox(width: 8), - Text('Archive'), - ], - ), - ), - const PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon(Icons.delete, color: Colors.red), - SizedBox(width: 8), - Text('Delete', style: TextStyle(color: Colors.red)), - ], - ), - ), - ], - ), - children: [ - // Today's intake times (if any) - only show in expanded view - if (todayIntakes.isNotEmpty) ...[ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: isCompletelyTaken - ? Colors.green.shade700.withValues(alpha: 0.8) - : Theme.of(context).colorScheme.secondaryContainer.withValues(alpha: 0.7), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isCompletelyTaken - ? Colors.green.shade500 - : Theme.of(context).colorScheme.secondary, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.onUndoLastTaken != null && isTakenToday) + _buildPopoverItem( + icon: Icons.undo, + label: 'Undo Last Taken', + onTap: () { + Navigator.of(context).pop(); + widget.onUndoLastTaken!(); + }, + color: Colors.orange, ), + _buildPopoverItem( + icon: Icons.edit, + label: 'Edit', + onTap: () { + Navigator.of(context).pop(); + widget.onEdit(); + }, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.check_circle_outline, - size: 16, - color: isCompletelyTaken - ? Colors.green.shade200 - : Theme.of(context).colorScheme.onSecondaryContainer, - ), - const SizedBox(width: 6), - Text( - 'Taken today:', + _buildPopoverItem( + icon: Icons.copy, + label: 'Duplicate', + onTap: () { + Navigator.of(context).pop(); + widget.onDuplicate(); + }, + ), + _buildPopoverItem( + icon: Icons.archive, + label: 'Archive', + color: Colors.orange, + onTap: () { + Navigator.of(context).pop(); + widget.onArchive(); + }, + ), + _buildPopoverItem( + icon: Icons.delete, + label: 'Delete', + color: Colors.red, + onTap: () { + Navigator.of(context).pop(); + widget.onDelete(); + }, + ), + ], + ), + ), + direction: PopoverDirection.bottom, + width: 180, + height: null, + arrowHeight: 0, + arrowWidth: 0, + backgroundColor: Colors.transparent, + shadow: const [], + ); + }, + borderRadius: BorderRadius.circular(12), + splashColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.4), + highlightColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: widget.showCompletionStatus ? (isCompletelyTaken + ? Theme.of(context).colorScheme.surface + : isTakenToday + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.surface) : Theme.of(context).colorScheme.surface, + border: Border.all( + color: widget.showCompletionStatus ? (isCompletelyTaken + ? Colors.green.shade600 + : isTakenToday + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.outline.withValues(alpha: 0.2)) : Theme.of(context).colorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: widget.showCompletionStatus ? (isCompletelyTaken + ? Colors.green.shade500 + : isTakenToday + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.primary.withValues(alpha: 0.1)) : Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + widget.showCompletionStatus ? (isCompletelyTaken + ? Icons.check_circle + : isTakenToday + ? Icons.schedule + : Icons.medication) : Icons.medication, + color: widget.showCompletionStatus ? (isCompletelyTaken + ? Theme.of(context).colorScheme.inverseSurface + : isTakenToday + ? Theme.of(context).colorScheme.onSecondary + : Theme.of(context).colorScheme.primary) : Theme.of(context).colorScheme.primary, + size: 18, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + widget.supplement.name, style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: isCompletelyTaken - ? Colors.green.shade200 - : Theme.of(context).colorScheme.onSecondaryContainer, + fontSize: 14, + fontWeight: FontWeight.bold, + color: widget.showCompletionStatus ? (isCompletelyTaken + ? Theme.of(context).colorScheme.inverseSurface + : isTakenToday + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.onSurface) : Theme.of(context).colorScheme.onSurface, + ), + ), + ), + if (widget.showCompletionStatus && isCompletelyTaken) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.green.shade500, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Complete', + style: TextStyle( + color: Theme.of(context).colorScheme.inverseSurface, + fontSize: 10, + fontWeight: FontWeight.bold, + ), ), ), ], + ], + ), + if (widget.supplement.brand != null && widget.supplement.brand!.isNotEmpty) + Text( + widget.supplement.brand!, + style: TextStyle( + fontSize: 11, + color: widget.showCompletionStatus ? (isCompletelyTaken + ? Colors.green.shade200 + : isTakenToday + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.primary) : Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500, + ), ), - const SizedBox(height: 6), - Wrap( - spacing: 8, - runSpacing: 4, - children: todayIntakes.map((intake) { - final units = intake['units'] as double; - final unitsText = units == 1.0 - ? widget.supplement.unitType - : '${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${widget.supplement.unitType}'; - - return Container( + const SizedBox(height: 4), + Row( + children: [ + Text( + '${widget.supplement.frequencyPerDay}x daily • ${widget.supplement.numberOfUnits} ${widget.supplement.unitType}', + style: TextStyle( + fontSize: 11, + color: ShadTheme.of(context).colorScheme.foreground.withValues(alpha: 0.7), + ), + ), + if (widget.showCompletionStatus && isTakenToday && !isCompletelyTaken) ...[ + const SizedBox(width: 8), + Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: isCompletelyTaken - ? Colors.green.shade600 - : Theme.of(context).colorScheme.secondary, - borderRadius: BorderRadius.circular(6), + color: Theme.of(context).colorScheme.secondary, + borderRadius: BorderRadius.circular(8), ), child: Text( - '${intake['time']} • $unitsText', + '$todayIntakeCount/${widget.supplement.frequencyPerDay}', style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w500, - color: isCompletelyTaken - ? Colors.white - : Theme.of(context).colorScheme.onSecondary, + color: Theme.of(context).colorScheme.onSecondary, + fontSize: 9, + fontWeight: FontWeight.bold, ), ), - ); - }).toList(), - ), - ], - ), - ), - const SizedBox(height: 16), - ], - - // Ingredients section - ShadCard( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ingredients per ${widget.supplement.unitType}:', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: ShadTheme.of(context).colorScheme.foreground, - ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 4, - children: widget.supplement.ingredients.map((ingredient) { - return ShadBadge( - child: Text( - '${ingredient.name} ${ingredient.amount}${ingredient.unit}', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - ), - ), - ); - }).toList(), - ), - ], - ), + ), + ], + ], + ), + ], ), ), - - const SizedBox(height: 12), - - // Schedule and dosage info - Row( - children: [ - Expanded( - child: ShadBadge( - child: Row( - children: [ - Icon( - Icons.schedule, - size: 14, - color: ShadTheme.of(context).colorScheme.foreground, - ), - const SizedBox(width: 4), - Text('${widget.supplement.frequencyPerDay}x daily'), - ], - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ShadBadge( - child: Row( - children: [ - Icon( - Icons.medication, - size: 14, - color: ShadTheme.of(context).colorScheme.foreground, - ), - const SizedBox(width: 4), - Text('${widget.supplement.numberOfUnits} ${widget.supplement.unitType}'), - ], - ), - ), - ), - ], - ), - - if (widget.supplement.reminderTimes.isNotEmpty) ...[ - const SizedBox(height: 8), - ShadBadge( - child: Row( - children: [ - Icon( - Icons.notifications, - size: 14, - color: ShadTheme.of(context).colorScheme.foreground, - ), - const SizedBox(width: 4), - Expanded( - child: Text( - 'Reminders: ${widget.supplement.reminderTimes.join(', ')}', - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ], - - if (widget.supplement.notes != null && widget.supplement.notes!.isNotEmpty) ...[ - const SizedBox(height: 8), - ShadCard( - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - widget.supplement.notes!, - style: TextStyle( - fontSize: 12, - color: ShadTheme.of(context).colorScheme.foreground, - fontStyle: FontStyle.italic, - ), - ), - ), - ), - ], - ], ), ), @@ -508,3 +362,4 @@ class _InfoChip extends StatelessWidget { ); } } + diff --git a/pubspec.lock b/pubspec.lock index 41c6664..fccff9e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -558,6 +558,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + popover: + dependency: "direct main" + description: + name: popover + sha256: "0606f3e10f92fc0459f5c52fd917738c29e7552323b28694d50c2d3312d0e1a2" + url: "https://pub.dev" + source: hosted + version: "0.3.1" provider: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 9fa3819..74d4a8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: # UI components shadcn_ui: ^0.29.2 + popover: ^0.3.0 # WebDAV sync functionality webdav_client: ^1.2.2