diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart index 369596c..ac1b370 100644 --- a/lib/screens/history_screen.dart +++ b/lib/screens/history_screen.dart @@ -15,12 +15,10 @@ class HistoryScreen extends StatefulWidget { class _HistoryScreenState extends State { int _selectedMonth = DateTime.now().month; int _selectedYear = DateTime.now().year; - DateTime? _selectedDay; @override void initState() { super.initState(); - _selectedDay = DateTime.now(); // Set today as the default selected day WidgetsBinding.instance.addPostFrameCallback((_) { context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); }); @@ -32,16 +30,33 @@ class _HistoryScreenState extends State { appBar: AppBar( title: const Text('Intake History'), backgroundColor: Theme.of(context).colorScheme.inversePrimary, + actions: [ + IconButton( + icon: const Icon(Icons.calendar_today), + onPressed: _showMonthPicker, + tooltip: 'Select Month', + ), + ], ), - body: _buildCalendarView(), + body: _buildCompactHistoryView(), ); } - Widget _buildCalendarView() { + Widget _buildCompactHistoryView() { return Column( children: [ + // Month selector header Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.2), + width: 1, + ), + ), + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -60,19 +75,14 @@ class _HistoryScreenState extends State { }); }, icon: const Icon(Icons.chevron_left), + iconSize: 20, ), - InkWell( - onTap: _showMonthPicker, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text( - DateFormat('MMMM yyyy').format(DateTime(_selectedYear, _selectedMonth)), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), + Text( + DateFormat('MMMM yyyy').format(DateTime(_selectedYear, _selectedMonth)), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, ), ), IconButton( @@ -93,72 +103,185 @@ class _HistoryScreenState extends State { } }, icon: const Icon(Icons.chevron_right), + iconSize: 20, ), ], ), ), + // History list Expanded( child: Consumer( builder: (context, provider, child) { - // Group intakes by date - final groupedIntakes = >>{}; - for (final intake in provider.monthlyIntakes) { - final date = DateTime.parse(intake['takenAt']); - final dateKey = DateFormat('yyyy-MM-dd').format(date); - groupedIntakes.putIfAbsent(dateKey, () => []); - groupedIntakes[dateKey]!.add(intake); + if (provider.monthlyIntakes.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.history, + size: 48, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + 'No intake history for this month', + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); } - return LayoutBuilder( - builder: (context, constraints) { - final isWideScreen = constraints.maxWidth > 800; + // Sort intakes by date (most recent first) + final sortedIntakes = List>.from(provider.monthlyIntakes) + ..sort((a, b) => DateTime.parse(b['takenAt']).compareTo(DateTime.parse(a['takenAt']))); - if (isWideScreen) { - // Desktop/tablet layout: side-by-side - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Calendar on the left - Expanded( - flex: 2, - child: Container( - margin: const EdgeInsets.all(16), - child: _buildCalendar(groupedIntakes), - ), - ), - // Selected day details on the right - Expanded( - flex: 3, - child: Container( - // add a bit more horizontal spacing between calendar and card - margin: const EdgeInsets.fromLTRB(8, 16, 16, 16), - child: SingleChildScrollView( - child: _buildSelectedDayDetails(groupedIntakes), + return ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: sortedIntakes.length, + itemBuilder: (context, index) { + final intake = sortedIntakes[index]; + final takenAt = DateTime.parse(intake['takenAt']); + final units = (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0; + + // Check if we need a date header + final previousIntake = index > 0 ? sortedIntakes[index - 1] : null; + final showDateHeader = previousIntake == null || + DateFormat('yyyy-MM-dd').format(DateTime.parse(previousIntake['takenAt'])) != + DateFormat('yyyy-MM-dd').format(takenAt); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showDateHeader) ...[ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + DateFormat('EEEE, MMMM d').format(takenAt), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, ), ), ), ], - ); - } else { - // Mobile layout: vertical stack - return SingleChildScrollView( - child: Column( - children: [ - // Calendar - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: _buildCalendar(groupedIntakes), + Dismissible( + key: Key('intake_${intake['id']}'), + direction: DismissDirection.endToStart, + background: Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20), + color: Colors.red.shade400, + child: const Icon( + Icons.delete, + color: Colors.white, ), - const SizedBox(height: 16), - // Selected day details - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: _buildSelectedDayDetails(groupedIntakes), + ), + confirmDismiss: (direction) async { + return await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Intake'), + content: Text('Delete ${intake['supplementName']} taken at ${DateFormat('HH:mm').format(takenAt)}?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Delete'), + ), + ], + ), + ); + }, + onDismissed: (direction) { + context.read().deleteIntake(intake['id']); + }, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), + child: Card( + margin: EdgeInsets.zero, + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: InkWell( + onTap: () => _deleteIntake(context, intake['id'], intake['supplementName']), + 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: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.medication, + color: Theme.of(context).colorScheme.primary, + size: 18, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + intake['supplementName'], + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + '${DateFormat('HH:mm').format(takenAt)} • ${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${intake['supplementUnitType'] ?? 'units'}', + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + if (intake['notes'] != null && intake['notes'].toString().isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + intake['notes'], + style: TextStyle( + fontSize: 11, + fontStyle: FontStyle.italic, + color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.8), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + Icon( + Icons.delete_outline, + size: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5), + ), + ], + ), + ), ), - ], + ), ), - ); - } + ), + ], + ); }, ); }, @@ -195,7 +318,6 @@ class _HistoryScreenState extends State { setState(() { _selectedMonth = picked.month; _selectedYear = picked.year; - _selectedDay = picked; // Set the selected day to the picked date }); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); @@ -243,549 +365,7 @@ class _HistoryScreenState extends State { ); } - Widget _buildCalendar(Map>> groupedIntakes) { - final firstDayOfMonth = DateTime(_selectedYear, _selectedMonth, 1); - final lastDayOfMonth = DateTime(_selectedYear, _selectedMonth + 1, 0); - final firstWeekday = firstDayOfMonth.weekday; - final daysInMonth = lastDayOfMonth.day; - // Calculate how many cells we need (including empty ones for alignment) - final totalCells = ((daysInMonth + firstWeekday - 1) / 7).ceil() * 7; - final weeks = (totalCells / 7).ceil(); - - return LayoutBuilder( - builder: (context, constraints) { - final isWideScreen = constraints.maxWidth > 800; - // Calculate calendar height based on number of weeks needed - final cellHeight = isWideScreen ? 56.0 : 48.0; - final calendarContentHeight = (weeks * cellHeight) + 60; // +60 for headers and padding - final calendarHeight = isWideScreen ? 400.0 : calendarContentHeight; - - return Card( - child: SizedBox( - height: calendarHeight, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Calendar header (weekdays) - Row( - children: [ - Expanded( - child: Center( - child: Text( - 'Mon', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: isWideScreen ? 14 : 12, - ), - ), - ), - ), - SizedBox(width: 4), - Expanded( - child: Center( - child: Text( - 'Tue', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: isWideScreen ? 14 : 12, - ), - ), - ), - ), - SizedBox(width: 4), - Expanded( - child: Center( - child: Text( - 'Wed', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: isWideScreen ? 14 : 12, - ), - ), - ), - ), - SizedBox(width: 4), - Expanded( - child: Center( - child: Text( - 'Thu', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: isWideScreen ? 14 : 12, - ), - ), - ), - ), - SizedBox(width: 4), - Expanded( - child: Center( - child: Text( - 'Fri', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: isWideScreen ? 14 : 12, - ), - ), - ), - ), - SizedBox(width: 4), - Expanded( - child: Center( - child: Text( - 'Sat', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: isWideScreen ? 14 : 12, - ), - ), - ), - ), - SizedBox(width: 4), - Expanded( - child: Center( - child: Text( - 'Sun', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: isWideScreen ? 14 : 12, - ), - ), - ), - ), - ], - ), - const SizedBox(height: 8), - // Calendar grid - Expanded( - child: GridView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 7, - childAspectRatio: 1.0, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - ), - itemCount: totalCells, - itemBuilder: (context, index) { - final dayNumber = index - firstWeekday + 2; - - if (dayNumber < 1 || dayNumber > daysInMonth) { - return const SizedBox.shrink(); // Empty cell - } - - final date = DateTime(_selectedYear, _selectedMonth, dayNumber); - final dateKey = DateFormat('yyyy-MM-dd').format(date); - final hasIntakes = groupedIntakes.containsKey(dateKey); - final intakeCount = hasIntakes ? groupedIntakes[dateKey]!.length : 0; - final isSelected = _selectedDay != null && - DateFormat('yyyy-MM-dd').format(_selectedDay!) == dateKey; - final isToday = DateFormat('yyyy-MM-dd').format(DateTime.now()) == dateKey; - - return GestureDetector( - onTap: () { - setState(() { - _selectedDay = date; - }); - }, - child: Container( - margin: const EdgeInsets.all(1), - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.primary - : hasIntakes - ? Theme.of(context).colorScheme.primaryContainer - : null, - border: isToday - ? Border.all(color: Theme.of(context).colorScheme.secondary, width: 2) - : null, - borderRadius: BorderRadius.circular(8), - ), - child: Stack( - children: [ - Center( - child: Text( - '$dayNumber', - style: TextStyle( - color: isSelected - ? Theme.of(context).colorScheme.onPrimary - : hasIntakes - ? Theme.of(context).colorScheme.onPrimaryContainer - : Theme.of(context).colorScheme.onSurface, - fontWeight: isToday ? FontWeight.bold : FontWeight.normal, - fontSize: isWideScreen ? 16 : 14, - ), - ), - ), - if (hasIntakes) - Positioned( - top: 2, - right: 2, - child: Container( - padding: EdgeInsets.all(isWideScreen ? 3 : 2), - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(8), - ), - constraints: BoxConstraints( - minWidth: isWideScreen ? 18 : 16, - minHeight: isWideScreen ? 18 : 16, - ), - child: Text( - '$intakeCount', - style: TextStyle( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onPrimary, - fontSize: isWideScreen ? 11 : 10, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - ), - ), - ], - ), - ), - ); - }, - ), - ), - ], - ), - ), - ), - ); - }, - ); - } - - Widget _buildSelectedDayDetails(Map>> groupedIntakes) { - return LayoutBuilder( - builder: (context, constraints) { - final isWideScreen = constraints.maxWidth > 600; - - if (_selectedDay == null) { - return Card( - child: Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.touch_app, - size: isWideScreen ? 64 : 48, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 16), - Text( - 'Tap a date on the calendar to see details', - style: TextStyle( - fontSize: isWideScreen ? 18 : 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ); - } - - final dateKey = DateFormat('yyyy-MM-dd').format(_selectedDay!); - final dayIntakes = groupedIntakes[dateKey] ?? []; - - if (dayIntakes.isEmpty) { - return Card( - child: Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.event_available, - size: isWideScreen ? 64 : 48, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 16), - Text( - 'No supplements taken on ${DateFormat('MMM d, yyyy').format(_selectedDay!)}', - style: TextStyle( - fontSize: isWideScreen ? 18 : 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ); - } - - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: double.infinity, - padding: EdgeInsets.all(isWideScreen ? 20 : 16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - DateFormat('EEEE, MMM d, yyyy').format(_selectedDay!), - style: TextStyle( - fontSize: isWideScreen ? 20 : 18, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - ), - const SizedBox(height: 4), - Text( - '${dayIntakes.length} supplement${dayIntakes.length != 1 ? 's' : ''} taken', - style: TextStyle( - fontSize: isWideScreen ? 16 : 14, - color: Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.8), - ), - ), - ], - ), - ), - Padding( - padding: EdgeInsets.all(isWideScreen ? 20 : 16), - child: Builder( - builder: (context) { - final settingsProvider = Provider.of(context, listen: false); - // Sort once per render - final sortedDayIntakes = List>.from(dayIntakes) - ..sort((a, b) => DateTime.parse(a['takenAt']).compareTo(DateTime.parse(b['takenAt']))); - // Helpers - String timeCategory(DateTime dt) { - final h = dt.hour; - if (h >= settingsProvider.morningStart && h <= settingsProvider.morningEnd) return 'morning'; - if (h >= settingsProvider.afternoonStart && h <= settingsProvider.afternoonEnd) return 'afternoon'; - if (h >= settingsProvider.eveningStart && h <= settingsProvider.eveningEnd) return 'evening'; - final ns = settingsProvider.nightStart; - final ne = settingsProvider.nightEnd; - final inNight = ns <= ne ? (h >= ns && h <= ne) : (h >= ns || h <= ne); - return inNight ? 'night' : 'anytime'; - } - String? sectionRange(String cat) { - switch (cat) { - case 'morning': - return settingsProvider.morningRange; - case 'afternoon': - return settingsProvider.afternoonRange; - case 'evening': - return settingsProvider.eveningRange; - case 'night': - return settingsProvider.nightRange; - default: - return null; - } - } - Widget headerFor(String cat) { - late final IconData icon; - late final Color color; - late final String title; - switch (cat) { - case 'morning': - icon = Icons.wb_sunny; - color = Colors.orange; - title = 'Morning'; - break; - case 'afternoon': - icon = Icons.light_mode; - color = Colors.blue; - title = 'Afternoon'; - break; - case 'evening': - icon = Icons.nightlight_round; - color = Colors.indigo; - title = 'Evening'; - break; - case 'night': - icon = Icons.bedtime; - color = Colors.purple; - title = 'Night'; - break; - default: - icon = Icons.schedule; - color = Colors.grey; - title = 'Anytime'; - } - final range = sectionRange(cat); - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: color.withValues(alpha: 0.3), - width: 1, - ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.2), - shape: BoxShape.circle, - ), - child: Icon( - icon, - size: 20, - color: color, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: color, - ), - ), - if (range != null) ...[ - Text( - '($range)', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: color.withValues(alpha: 0.8), - ), - ), - ], - ], - ), - ), - ], - ), - ); - } - // Build a non-scrollable list so the card auto-expands to fit content - final List children = []; - for (int index = 0; index < sortedDayIntakes.length; index++) { - final intake = sortedDayIntakes[index]; - final takenAt = DateTime.parse(intake['takenAt']); - final units = (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0; - final currentCategory = timeCategory(takenAt); - final needsHeader = index == 0 - ? true - : currentCategory != timeCategory(DateTime.parse(sortedDayIntakes[index - 1]['takenAt'])); - if (needsHeader) { - children.add(headerFor(currentCategory)); - } - children.add( - Card( - margin: const EdgeInsets.only(bottom: 12), - elevation: 2, - child: Padding( - padding: EdgeInsets.all(isWideScreen ? 16 : 12), - child: Row( - children: [ - CircleAvatar( - backgroundColor: Theme.of(context).colorScheme.primary, - radius: isWideScreen ? 24 : 20, - child: Icon( - Icons.medication, - color: Theme.of(context).colorScheme.onPrimary, - size: isWideScreen ? 24 : 20, - ), - ), - SizedBox(width: isWideScreen ? 16 : 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - intake['supplementName'], - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: isWideScreen ? 16 : 14, - ), - ), - const SizedBox(height: 4), - Text( - '${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${intake['supplementUnitType'] ?? 'units'} at ${DateFormat('HH:mm').format(takenAt)}', - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500, - fontSize: isWideScreen ? 14 : 12, - ), - ), - if (intake['notes'] != null && intake['notes'].toString().isNotEmpty) ...[ - const SizedBox(height: 4), - Text( - intake['notes'], - style: TextStyle( - fontSize: isWideScreen ? 13 : 12, - fontStyle: FontStyle.italic, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ], - ), - ), - IconButton( - icon: Icon( - Icons.delete_outline, - color: Colors.red.shade400, - size: isWideScreen ? 24 : 20, - ), - onPressed: () => _deleteIntake(context, intake['id'], intake['supplementName']), - tooltip: 'Delete intake', - ), - ], - ), - ), - ), - ); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: children, - ); - }, - ), - ), - ], - ), - ); - }, - ); - } @override void dispose() {