diff --git a/lib/screens/athlete_detail_screen.dart b/lib/screens/athlete_detail_screen.dart index 82ea19c..c20df3e 100644 --- a/lib/screens/athlete_detail_screen.dart +++ b/lib/screens/athlete_detail_screen.dart @@ -6,6 +6,7 @@ import '../models/workout_session.dart'; import '../widgets/workout_card.dart'; import '../widgets/video_player.dart'; import '../widgets/recorded_video_tile.dart'; +import 'calendar_screen.dart'; class AthleteDetailScreen extends StatelessWidget { final String athleteUid; @@ -20,7 +21,7 @@ class AthleteDetailScreen extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultTabController( - length: 3, + length: 4, child: Scaffold( appBar: AppBar( title: Text(athleteName), @@ -29,6 +30,7 @@ class AthleteDetailScreen extends StatelessWidget { Tab(icon: Icon(Icons.folder_copy), text: 'Programs'), Tab(icon: Icon(Icons.fitness_center), text: 'Workouts'), Tab(icon: Icon(Icons.history), text: 'Sessions'), + Tab(icon: Icon(Icons.calendar_month), text: 'Calendar'), ], ), ), @@ -37,6 +39,7 @@ class AthleteDetailScreen extends StatelessWidget { _AthleteProgramsTab(athleteUid: athleteUid), _AthleteWorkoutsTab(athleteUid: athleteUid), _AthleteSessionsTab(athleteUid: athleteUid), + _AthleteCalendarTab(athleteUid: athleteUid), ], ), ), @@ -135,7 +138,10 @@ class _ProgramTile extends StatelessWidget { children: docs.map((doc) { final workout = Workout.fromMap( doc.id, doc.data() as Map); - return _WorkoutExpansionTile(workout: workout); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: WorkoutCard(workout: workout, readOnly: true), + ); }).toList(), ); }, @@ -146,75 +152,6 @@ class _ProgramTile extends StatelessWidget { } } -class _WorkoutExpansionTile extends StatelessWidget { - final Workout workout; - const _WorkoutExpansionTile({required this.workout}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return ExpansionTile( - tilePadding: const EdgeInsets.symmetric(horizontal: 16), - leading: const Icon(Icons.fitness_center, size: 18, color: Colors.teal), - title: Text(workout.name, style: const TextStyle(fontSize: 14)), - subtitle: Text( - '${workout.exercises.length} exercise(s) · ${workout.type}${workout.schedule != null ? ' · ${workout.schedule}' : ''}', - style: theme.textTheme.bodySmall, - ), - children: workout.exercises.isEmpty - ? [ - Padding( - padding: const EdgeInsets.fromLTRB(32, 4, 16, 12), - child: Text('No exercises defined', - style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey)), - ), - ] - : workout.exercises.map((ex) => _AthleteExerciseRow(ex: ex)).toList(), - ); - } -} - -class _AthleteExerciseRow extends StatelessWidget { - final dynamic ex; - const _AthleteExerciseRow({required this.ex}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final hasVideo = ex.videoUrl != null && (ex.videoUrl as String).isNotEmpty; - final hasNotes = ex.notes != null && (ex.notes as String).isNotEmpty; - - return Padding( - padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '• ${ex.name} ${ex.sets}×${ex.reps ?? '—'}${ex.weight != null ? ' @ ${ex.weight}' : ''}', - style: theme.textTheme.bodySmall, - ), - if (hasVideo) - VideoLinkTile( - url: ex.videoUrl as String, - title: 'Reference: ${ex.name}', - ), - if (hasNotes) - Padding( - padding: const EdgeInsets.only(left: 10, top: 2), - child: Text( - ex.notes as String, - style: theme.textTheme.bodySmall?.copyWith( - color: Colors.grey[600], - fontStyle: FontStyle.italic, - ), - ), - ), - ], - ), - ); - } -} - class _AthleteWorkoutsTab extends StatelessWidget { final String athleteUid; const _AthleteWorkoutsTab({required this.athleteUid}); @@ -462,3 +399,191 @@ class _SessionEntryTile extends StatelessWidget { ); } } + +/// Read-only calendar tab for coach view of athlete. +class _AthleteCalendarTab extends StatelessWidget { + final String athleteUid; + const _AthleteCalendarTab({required this.athleteUid}); + + @override + Widget build(BuildContext context) { + return _ReadOnlyCalendar(athleteUid: athleteUid); + } +} + +class _ReadOnlyCalendar extends StatefulWidget { + final String athleteUid; + const _ReadOnlyCalendar({required this.athleteUid}); + + @override + State<_ReadOnlyCalendar> createState() => _ReadOnlyCalendarState(); +} + +class _ReadOnlyCalendarState extends State<_ReadOnlyCalendar> { + DateTime _focusedMonth = DateTime(DateTime.now().year, DateTime.now().month); + DateTime _selectedDate = DateTime( + DateTime.now().year, + DateTime.now().month, + DateTime.now().day, + ); + + void _changeMonth(int delta) { + setState(() { + _focusedMonth = DateTime( + _focusedMonth.year, + _focusedMonth.month + delta, + ); + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final months = [ + 'January','February','March','April','May','June', + 'July','August','September','October','November','December' + ]; + + return StreamBuilder( + stream: FirebaseFirestore.instance + .collection('users') + .doc(widget.athleteUid) + .collection('sessions') + .snapshots(), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + final sessions = snapshot.data!.docs.map((doc) { + return WorkoutSession.fromMap( + doc.id, + doc.data()! as Map, + ); + }).toList(); + + final sessionsByDate = >{}; + for (final session in sessions) { + final date = session.calendarDate; + if (date == null) continue; + final key = DateTime(date.year, date.month, date.day); + sessionsByDate.putIfAbsent(key, () => []).add(session); + } + + final selectedSessions = sessionsByDate[_selectedDate] ?? []; + + final firstDay = DateTime(_focusedMonth.year, _focusedMonth.month, 1); + final lastDay = DateTime(_focusedMonth.year, _focusedMonth.month + 1, 0); + final startWeekday = firstDay.weekday % 7; + final daysInMonth = lastDay.day; + final today = DateTime.now(); + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: () => _changeMonth(-1), + ), + Text( + '${months[_focusedMonth.month - 1]} ${_focusedMonth.year}', + style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + IconButton( + icon: const Icon(Icons.chevron_right), + onPressed: () => _changeMonth(1), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: ['S','M','T','W','T','F','S'].map((d) => + Expanded(child: Center(child: Text(d, style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey)))) + ).toList(), + ), + ), + const SizedBox(height: 4), + Expanded( + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + childAspectRatio: 1, + ), + itemCount: startWeekday + daysInMonth, + itemBuilder: (context, index) { + if (index < startWeekday) return const SizedBox(); + final day = index - startWeekday + 1; + final date = DateTime(_focusedMonth.year, _focusedMonth.month, day); + final hasSession = sessionsByDate.containsKey(date); + final isToday = date.year == today.year && date.month == today.month && date.day == today.day; + final isSelected = date.year == _selectedDate.year && + date.month == _selectedDate.month && date.day == _selectedDate.day; + + return GestureDetector( + onTap: () => setState(() => _selectedDate = date), + child: Container( + margin: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: isSelected ? Colors.teal.withAlpha(50) : null, + border: isToday ? Border.all(color: Colors.teal, width: 2) : null, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + '$day', + style: TextStyle( + color: hasSession ? Colors.teal : null, + fontWeight: hasSession ? FontWeight.bold : null, + ), + ), + ), + ), + ); + }, + ), + ), + if (selectedSessions.isNotEmpty) + Expanded( + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: selectedSessions.length, + itemBuilder: (context, index) { + final session = selectedSessions[index]; + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: Icon( + session.isCompleted ? Icons.check_circle : Icons.schedule, + color: session.isCompleted ? Colors.teal : Colors.orange, + ), + title: Text(session.planName ?? 'Workout'), + subtitle: Text(session.dayName ?? ''), + ), + ); + }, + ), + ) + else + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'No workouts on this day', + style: theme.textTheme.bodyMedium?.copyWith(color: Colors.grey), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/screens/workout_detail_screen.dart b/lib/screens/workout_detail_screen.dart index 532b8f4..9a0d849 100644 --- a/lib/screens/workout_detail_screen.dart +++ b/lib/screens/workout_detail_screen.dart @@ -7,8 +7,9 @@ import 'session_edit_screen.dart'; class WorkoutDetailScreen extends StatefulWidget { final Workout workout; + final bool readOnly; - const WorkoutDetailScreen({super.key, required this.workout}); + const WorkoutDetailScreen({super.key, required this.workout, this.readOnly = false}); @override State createState() => _WorkoutDetailScreenState(); @@ -34,13 +35,14 @@ class _WorkoutDetailScreenState extends State { appBar: AppBar( title: Text(widget.workout.name), actions: [ - IconButton( - icon: const Icon(Icons.edit_outlined), - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => WorkoutEditScreen(workout: widget.workout)), + if (!widget.readOnly) + IconButton( + icon: const Icon(Icons.edit_outlined), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => WorkoutEditScreen(workout: widget.workout)), + ), ), - ), ], ), body: ListView( @@ -65,16 +67,17 @@ class _WorkoutDetailScreenState extends State { ), ], const Spacer(), - FilledButton.icon( - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => SessionEditScreen(workout: widget.workout), + if (!widget.readOnly) + FilledButton.icon( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => SessionEditScreen(workout: widget.workout), + ), ), + icon: const Icon(Icons.play_arrow, size: 18), + label: const Text('Start'), ), - icon: const Icon(Icons.play_arrow, size: 18), - label: const Text('Start'), - ), ], ), if (widget.workout.description != null && widget.workout.description!.isNotEmpty) ...[ diff --git a/lib/widgets/plan_card.dart b/lib/widgets/plan_card.dart index 4a999ec..59021e0 100644 --- a/lib/widgets/plan_card.dart +++ b/lib/widgets/plan_card.dart @@ -6,8 +6,9 @@ import 'video_player.dart'; class PlanCard extends StatelessWidget { final WorkoutPlan plan; + final bool readOnly; - const PlanCard({super.key, required this.plan}); + const PlanCard({super.key, required this.plan, this.readOnly = false}); Future _deletePlan(BuildContext context) async { final confirmed = await showDialog( @@ -54,13 +55,14 @@ class PlanCard extends StatelessWidget { Expanded( child: Text(plan.name, style: const TextStyle(fontWeight: FontWeight.bold)), ), - IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20), - onPressed: () => _deletePlan(context), - tooltip: 'Delete Workout', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), + if (!readOnly) + IconButton( + icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20), + onPressed: () => _deletePlan(context), + tooltip: 'Delete Workout', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), ], ), subtitle: Text( diff --git a/lib/widgets/session_card.dart b/lib/widgets/session_card.dart index 04c6f00..9cf2810 100644 --- a/lib/widgets/session_card.dart +++ b/lib/widgets/session_card.dart @@ -6,8 +6,9 @@ import '../screens/session_detail_screen.dart'; class SessionCard extends StatelessWidget { final WorkoutSession session; + final bool readOnly; - const SessionCard({super.key, required this.session}); + const SessionCard({super.key, required this.session, this.readOnly = false}); String _formatDate(DateTime? date) { if (date == null) return 'Unknown date'; @@ -79,11 +80,12 @@ class SessionCard extends StatelessWidget { ), ), ), - IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20), - onPressed: () => _deleteSession(context), - tooltip: 'Delete Session', - ), + if (!readOnly) + IconButton( + icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20), + onPressed: () => _deleteSession(context), + tooltip: 'Delete Session', + ), ], ), Row( diff --git a/lib/widgets/workout_card.dart b/lib/widgets/workout_card.dart index d792a32..3483d67 100644 --- a/lib/widgets/workout_card.dart +++ b/lib/widgets/workout_card.dart @@ -105,7 +105,7 @@ class _WorkoutCardState extends State { icon: const Icon(Icons.open_in_new, size: 16), label: const Text('View Details'), onPressed: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => WorkoutDetailScreen(workout: widget.workout)), + MaterialPageRoute(builder: (_) => WorkoutDetailScreen(workout: widget.workout, readOnly: widget.readOnly)), ), ), if (!widget.readOnly) ...[