diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart index b2e638e..2d9513e 100644 --- a/lib/screens/history_screen.dart +++ b/lib/screens/history_screen.dart @@ -10,17 +10,15 @@ class HistoryScreen extends StatefulWidget { State createState() => _HistoryScreenState(); } -class _HistoryScreenState extends State with SingleTickerProviderStateMixin { - late TabController _tabController; - DateTime _selectedDate = DateTime.now(); +class _HistoryScreenState extends State { int _selectedMonth = DateTime.now().month; int _selectedYear = DateTime.now().year; - int _refreshKey = 0; // Add this to force FutureBuilder refresh + DateTime? _selectedDay; @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this); + _selectedDay = DateTime.now(); // Set today as the default selected day WidgetsBinding.instance.addPostFrameCallback((_) { context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); }); @@ -32,211 +30,12 @@ class _HistoryScreenState extends State with SingleTickerProvider appBar: AppBar( title: const Text('Intake History'), backgroundColor: Theme.of(context).colorScheme.inversePrimary, - bottom: TabBar( - controller: _tabController, - tabs: const [ - Tab(text: 'Daily View'), - Tab(text: 'Monthly View'), - ], - ), - ), - body: TabBarView( - controller: _tabController, - children: [ - _buildDailyView(), - _buildMonthlyView(), - ], ), + body: _buildCalendarView(), ); } - Widget _buildDailyView() { - return Column( - children: [ - Container( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - onPressed: () { - setState(() { - _selectedDate = _selectedDate.subtract(const Duration(days: 1)); - }); - }, - icon: const Icon(Icons.chevron_left), - ), - InkWell( - onTap: _selectDate, - child: Text( - DateFormat('EEEE, MMM d, yyyy').format(_selectedDate), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - IconButton( - onPressed: _selectedDate.isBefore(DateTime.now()) - ? () { - setState(() { - _selectedDate = _selectedDate.add(const Duration(days: 1)); - }); - } - : null, - icon: const Icon(Icons.chevron_right), - ), - ], - ), - ), - Expanded( - child: FutureBuilder>>( - key: ValueKey('daily_view_$_refreshKey'), // Use refresh key to force rebuild - future: context.read().getIntakesForDate(_selectedDate), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.event_note, - size: 64, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 16), - Text( - 'No supplements taken on this day', - style: TextStyle( - fontSize: 18, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ); - } - - final intakes = snapshot.data!; - - // Group intakes by supplement - final Map>> groupedIntakes = {}; - for (final intake in intakes) { - final supplementName = intake['supplementName'] as String; - groupedIntakes.putIfAbsent(supplementName, () => []); - groupedIntakes[supplementName]!.add(intake); - } - - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: groupedIntakes.length, - itemBuilder: (context, index) { - final supplementName = groupedIntakes.keys.elementAt(index); - final supplementIntakes = groupedIntakes[supplementName]!; - - // Calculate totals - double totalUnits = 0; - final firstIntake = supplementIntakes.first; - - for (final intake in supplementIntakes) { - totalUnits += (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0; - } - - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: ExpansionTile( - leading: CircleAvatar( - backgroundColor: Theme.of(context).colorScheme.primary, - child: Icon(Icons.medication, color: Theme.of(context).colorScheme.onPrimary), - ), - title: Text( - supplementName, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${totalUnits.toStringAsFixed(totalUnits % 1 == 0 ? 0 : 1)} ${firstIntake['supplementUnitType'] ?? 'units'} total', - style: TextStyle( - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary, - ), - ), - Text( - '${supplementIntakes.length} intake${supplementIntakes.length > 1 ? 's' : ''}', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - children: supplementIntakes.map((intake) { - final takenAt = DateTime.parse(intake['takenAt']); - final units = (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0; - - return ListTile( - contentPadding: const EdgeInsets.only(left: 72, right: 8), - title: Text( - '${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${intake['supplementUnitType'] ?? 'units'}', - style: const TextStyle(fontSize: 14), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${intake['supplementUnitType'] ?? 'units'} at ${DateFormat('HH:mm').format(takenAt)}', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - if (intake['notes'] != null && intake['notes'].toString().isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 4), - child: Text( - intake['notes'], - style: TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - trailing: IconButton( - icon: Icon( - Icons.delete_outline, - color: Colors.red.shade400, - size: 20, - ), - onPressed: () => _deleteIntake(context, intake['id'], intake['supplementName']), - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - ), - ); - }).toList(), - ), - ); - }, - ); - }, - ), - ), - ], - ); - } - - Widget _buildMonthlyView() { + Widget _buildCalendarView() { return Column( children: [ Container( @@ -258,11 +57,18 @@ class _HistoryScreenState extends State with SingleTickerProvider }, icon: const Icon(Icons.chevron_left), ), - Text( - DateFormat('MMMM yyyy').format(DateTime(_selectedYear, _selectedMonth)), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + 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, + ), + ), ), ), IconButton( @@ -288,29 +94,6 @@ class _HistoryScreenState extends State with SingleTickerProvider Expanded( child: Consumer( builder: (context, provider, child) { - if (provider.monthlyIntakes.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.calendar_month, - size: 64, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 16), - Text( - 'No supplements taken this month', - style: TextStyle( - fontSize: 18, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ); - } - // Group intakes by date final groupedIntakes = >>{}; for (final intake in provider.monthlyIntakes) { @@ -320,81 +103,50 @@ class _HistoryScreenState extends State with SingleTickerProvider groupedIntakes[dateKey]!.add(intake); } - final sortedDates = groupedIntakes.keys.toList() - ..sort((a, b) => b.compareTo(a)); // Most recent first - - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: sortedDates.length, - itemBuilder: (context, index) { - final dateKey = sortedDates[index]; - final dayIntakes = groupedIntakes[dateKey]!; - final date = DateTime.parse(dateKey); - - return Card( - margin: const EdgeInsets.only(bottom: 16), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - DateFormat('EEEE, MMM d').format(date), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + return LayoutBuilder( + builder: (context, constraints) { + final isWideScreen = constraints.maxWidth > 800; + + 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), ), - const SizedBox(height: 8), - ...dayIntakes.map((intake) { - final takenAt = DateTime.parse(intake['takenAt']); - final units = (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0; - return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( - children: [ - Icon( - Icons.circle, - size: 8, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - '${intake['supplementName']} - ${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${intake['supplementUnitType'] ?? 'units'} at ${DateFormat('HH:mm').format(takenAt)}', - style: const TextStyle(fontSize: 14), - ), - ), - IconButton( - icon: Icon( - Icons.delete_outline, - color: Colors.red.shade400, - size: 18, - ), - onPressed: () => _deleteIntake(context, intake['id'], intake['supplementName']), - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 24, - minHeight: 24, - ), - ), - ], - ), - ); - }), - const SizedBox(height: 4), - Text( - '${dayIntakes.length} supplement${dayIntakes.length != 1 ? 's' : ''} taken', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w500, - ), + ), + // Selected day details on the right + Expanded( + flex: 3, + child: Container( + margin: const EdgeInsets.fromLTRB(0, 16, 16, 16), + child: _buildSelectedDayDetails(groupedIntakes), ), - ], - ), - ), - ); + ), + ], + ); + } else { + // Mobile layout: vertical stack + return Column( + children: [ + // Calendar + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: _buildCalendar(groupedIntakes), + ), + const SizedBox(height: 16), + // Selected day details + Expanded( + child: _buildSelectedDayDetails(groupedIntakes), + ), + ], + ); + } }, ); }, @@ -404,17 +156,35 @@ class _HistoryScreenState extends State with SingleTickerProvider ); } - void _selectDate() async { + void _showMonthPicker() async { + final now = DateTime.now(); final picked = await showDatePicker( context: context, - initialDate: _selectedDate, + initialDate: DateTime(_selectedYear, _selectedMonth), firstDate: DateTime(2020), - lastDate: DateTime.now(), + lastDate: now, + initialDatePickerMode: DatePickerMode.year, + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + dialogTheme: DialogThemeData( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + ), + ), + child: child!, + ); + }, ); + if (picked != null) { setState(() { - _selectedDate = picked; + _selectedMonth = picked.month; + _selectedYear = picked.year; + _selectedDay = picked; // Set the selected day to the picked date }); + context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); } } @@ -435,15 +205,10 @@ class _HistoryScreenState extends State with SingleTickerProvider Navigator.of(context).pop(); // Force refresh of the UI - setState(() { - _refreshKey++; // This will force FutureBuilder to rebuild - }); + setState(() {}); // Force refresh of the current view data - if (_tabController.index == 1) { - // Monthly view - refresh monthly intakes - context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); - } + context.read().loadMonthlyIntakes(_selectedYear, _selectedMonth); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -460,9 +225,344 @@ class _HistoryScreenState extends State with SingleTickerProvider ); } + 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: Container( + height: calendarHeight, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Calendar header (weekdays) + Row( + children: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + .map((day) => Expanded( + child: Center( + child: Text( + day, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: isWideScreen ? 14 : 12, + ), + ), + ), + )) + .toList(), + ), + 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(); // 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.withOpacity(0.8), + ), + ), + ], + ), + ), + Expanded( + child: ListView.builder( + padding: EdgeInsets.all(isWideScreen ? 20 : 16), + itemCount: dayIntakes.length, + itemBuilder: (context, index) { + final intake = dayIntakes[index]; + final takenAt = DateTime.parse(intake['takenAt']); + final units = (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0; + + return 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', + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + }, + ); + } + @override void dispose() { - _tabController.dispose(); super.dispose(); } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 74829ce..9f3a721 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -200,6 +200,7 @@ class SettingsScreen extends StatelessWidget { ButtonSegment(value: 1, label: Text('1')), ButtonSegment(value: 2, label: Text('2')), ButtonSegment(value: 3, label: Text('3')), + ButtonSegment(value: 4, label: Text('4')), ButtonSegment(value: 5, label: Text('5')), ], selected: {settingsProvider.maxRetryAttempts},