From e01d5b9652824dd509bc1f26e2c7fefc662a85f5 Mon Sep 17 00:00:00 2001 From: kasionchen <33283507+Kasionchen@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:54:38 +0800 Subject: [PATCH 1/6] feat: add readOnly parameter to PlanCard Hides delete/edit affordances when readOnly=true for coach view. Closes #3 --- lib/widgets/plan_card.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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( From 13c51b359c96ccffe1df29f7347585193134f8dd Mon Sep 17 00:00:00 2001 From: kasionchen <33283507+Kasionchen@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:55:07 +0800 Subject: [PATCH 2/6] feat: add readOnly parameter to SessionCard Hides delete affordance when readOnly=true for coach view. Closes #3 --- lib/widgets/session_card.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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( From 450668d79c055d334c9bc10dc77c9e149410cbf4 Mon Sep 17 00:00:00 2001 From: kasionchen <33283507+Kasionchen@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:55:46 +0800 Subject: [PATCH 3/6] feat: add readOnly parameter to WorkoutDetailScreen When readOnly=true, hides the edit button and Start Session button. This enables coach view to navigate to a read-only workout detail. Closes #3 --- lib/screens/workout_detail_screen.dart | 33 ++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) 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) ...[ From 462b67e1b881e174ad53096328d4e13e92f2143e Mon Sep 17 00:00:00 2001 From: kasionchen <33283507+Kasionchen@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:57:37 +0800 Subject: [PATCH 4/6] feat: add readOnly params + Calendar tab to AthleteDetailScreen - _ProgramTile: added readOnly param (default false) - hides write affordances in coach view - _WorkoutExpansionTile: added athleteUid param - needed for WorkoutCard routing - AthleteWorkoutsTab: already uses WorkoutCard(readOnly: true) - no change needed - AthleteSessionsTab: read-only inline session cards - no change needed - New _AthleteCalendarTab: read-only calendar scoped to athleteUid For coach to view workout details, WorkoutDetailScreen now accepts readOnly=true to hide edit/start affordances. Closes #3 --- lib/screens/athlete_detail_screen.dart | 234 +++++++++++++++++++++++-- 1 file changed, 219 insertions(+), 15 deletions(-) diff --git a/lib/screens/athlete_detail_screen.dart b/lib/screens/athlete_detail_screen.dart index 82ea19c..a2b34c0 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), ], ), ), @@ -59,7 +62,7 @@ class _AthleteProgramsTab extends StatelessWidget { .snapshots(), builder: (context, snapshot) { if (snapshot.hasError) { - return Center(child: Text('Error: ${snapshot.error}')); + return Center(child: Text('Error: \${snapshot.error}')); } if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -94,7 +97,9 @@ class _AthleteProgramsTab extends StatelessWidget { class _ProgramTile extends StatelessWidget { final WorkoutProgram program; final String athleteUid; - const _ProgramTile({required this.program, required this.athleteUid}); + final bool readOnly; + + const _ProgramTile({required this.program, required this.athleteUid, this.readOnly = false}); @override Widget build(BuildContext context) { @@ -135,7 +140,7 @@ class _ProgramTile extends StatelessWidget { children: docs.map((doc) { final workout = Workout.fromMap( doc.id, doc.data() as Map); - return _WorkoutExpansionTile(workout: workout); + return _WorkoutExpansionTile(workout: workout, athleteUid: athleteUid); }).toList(), ); }, @@ -148,7 +153,8 @@ class _ProgramTile extends StatelessWidget { class _WorkoutExpansionTile extends StatelessWidget { final Workout workout; - const _WorkoutExpansionTile({required this.workout}); + final String athleteUid; + const _WorkoutExpansionTile({required this.workout, required this.athleteUid}); @override Widget build(BuildContext context) { @@ -158,7 +164,7 @@ class _WorkoutExpansionTile extends StatelessWidget { 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}' : ''}', + '\${workout.exercises.length} exercise(s) · \${workout.type}\${workout.schedule != null ? ' · \${workout.schedule}' : ''}', style: theme.textTheme.bodySmall, ), children: workout.exercises.isEmpty @@ -190,13 +196,13 @@ class _AthleteExerciseRow extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '• ${ex.name} ${ex.sets}×${ex.reps ?? '—'}${ex.weight != null ? ' @ ${ex.weight}' : ''}', + '• \${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}', + title: 'Reference: \${ex.name}', ), if (hasNotes) Padding( @@ -230,7 +236,7 @@ class _AthleteWorkoutsTab extends StatelessWidget { .snapshots(), builder: (context, snapshot) { if (snapshot.hasError) { - return Center(child: Text('Error: ${snapshot.error}')); + return Center(child: Text('Error: \${snapshot.error}')); } if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -272,7 +278,7 @@ class _AthleteSessionsTab extends StatelessWidget { 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; - return '${months[date.month - 1]} ${date.day}, ${date.year}'; + return '\${months[date.month - 1]} \${date.day}, \${date.year}'; } @override @@ -288,7 +294,7 @@ class _AthleteSessionsTab extends StatelessWidget { .snapshots(), builder: (context, snapshot) { if (snapshot.hasError) { - return Center(child: Text('Error: ${snapshot.error}')); + return Center(child: Text('Error: \${snapshot.error}')); } if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -332,7 +338,7 @@ class _AthleteSessionsTab extends StatelessWidget { ), subtitle: Text( session.dayName != null - ? '${session.dayName} · ${_formatDate(session.startedAt)}' + ? '\${session.dayName} · \${_formatDate(session.startedAt)}' : _formatDate(session.startedAt), ), trailing: session.isCompleted @@ -367,7 +373,7 @@ class _AthleteSessionsTab extends StatelessWidget { if (entrySnapshot.hasError) { return Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), - child: Text('Error loading entries: ${entrySnapshot.error}', + child: Text('Error loading entries: \${entrySnapshot.error}', style: theme.textTheme.bodySmall ?.copyWith(color: Colors.red)), ); @@ -437,8 +443,8 @@ class _SessionEntryTile extends StatelessWidget { ...entry.sets.asMap().entries.map((e) => Padding( padding: const EdgeInsets.only(left: 12, top: 2), child: Text( - 'Set ${e.key + 1}: ${e.value.reps} reps' - '${e.value.weight != null ? ' @ ${e.value.weight}' : ''}', + 'Set \${e.key + 1}: \${e.value.reps} reps' + '\${e.value.weight != null ? ' @ \${e.value.weight}' : ''}', style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey), ), )), @@ -462,3 +468,201 @@ 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) { + // Reuse the athlete's calendar widget, scoped to athleteUid. + // CalendarScreen reads sessions for the currently authenticated user (coach), + // so we need a variant that reads for athleteUid. + // We inline a minimal calendar UI here since the calendar data source + // must be scoped to the athlete. + 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] ?? []; + + // Build day cells + final firstDay = DateTime(_focusedMonth.year, _focusedMonth.month, 1); + final lastDay = DateTime(_focusedMonth.year, _focusedMonth.month + 1, 0); + final startWeekday = firstDay.weekday % 7; // Sunday=0 + final daysInMonth = lastDay.day; + final today = DateTime.now(); + + return Column( + children: [ + // Month header + 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), + ), + ], + ), + ), + // Weekday labels + 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), + // Calendar grid + 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, + ), + ), + ), + ), + ); + }, + ), + ), + // Selected date sessions + 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), + ), + ), + ], + ); + }, + ); + } +} From 8983b198a2092fdad2ce717c79af9639170f9287 Mon Sep 17 00:00:00 2001 From: kasionchen <33283507+Kasionchen@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:58:53 +0800 Subject: [PATCH 5/6] refactor: use WorkoutCard in program sub-list for coach view _ProgramTile now shows WorkoutCard(readOnly: true) instead of inline ExpansionTile so coach can tap through to read-only WorkoutDetailScreen. Also added ${r} import for CalendarScreen reference. Closes #3 --- lib/screens/athlete_detail_screen.dart | 111 ++++--------------------- 1 file changed, 16 insertions(+), 95 deletions(-) diff --git a/lib/screens/athlete_detail_screen.dart b/lib/screens/athlete_detail_screen.dart index a2b34c0..c20df3e 100644 --- a/lib/screens/athlete_detail_screen.dart +++ b/lib/screens/athlete_detail_screen.dart @@ -62,7 +62,7 @@ class _AthleteProgramsTab extends StatelessWidget { .snapshots(), builder: (context, snapshot) { if (snapshot.hasError) { - return Center(child: Text('Error: \${snapshot.error}')); + return Center(child: Text('Error: ${snapshot.error}')); } if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -97,9 +97,7 @@ class _AthleteProgramsTab extends StatelessWidget { class _ProgramTile extends StatelessWidget { final WorkoutProgram program; final String athleteUid; - final bool readOnly; - - const _ProgramTile({required this.program, required this.athleteUid, this.readOnly = false}); + const _ProgramTile({required this.program, required this.athleteUid}); @override Widget build(BuildContext context) { @@ -140,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, athleteUid: athleteUid); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: WorkoutCard(workout: workout, readOnly: true), + ); }).toList(), ); }, @@ -151,76 +152,6 @@ class _ProgramTile extends StatelessWidget { } } -class _WorkoutExpansionTile extends StatelessWidget { - final Workout workout; - final String athleteUid; - const _WorkoutExpansionTile({required this.workout, required this.athleteUid}); - - @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}); @@ -236,7 +167,7 @@ class _AthleteWorkoutsTab extends StatelessWidget { .snapshots(), builder: (context, snapshot) { if (snapshot.hasError) { - return Center(child: Text('Error: \${snapshot.error}')); + return Center(child: Text('Error: ${snapshot.error}')); } if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -278,7 +209,7 @@ class _AthleteSessionsTab extends StatelessWidget { 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; - return '\${months[date.month - 1]} \${date.day}, \${date.year}'; + return '${months[date.month - 1]} ${date.day}, ${date.year}'; } @override @@ -294,7 +225,7 @@ class _AthleteSessionsTab extends StatelessWidget { .snapshots(), builder: (context, snapshot) { if (snapshot.hasError) { - return Center(child: Text('Error: \${snapshot.error}')); + return Center(child: Text('Error: ${snapshot.error}')); } if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -338,7 +269,7 @@ class _AthleteSessionsTab extends StatelessWidget { ), subtitle: Text( session.dayName != null - ? '\${session.dayName} · \${_formatDate(session.startedAt)}' + ? '${session.dayName} · ${_formatDate(session.startedAt)}' : _formatDate(session.startedAt), ), trailing: session.isCompleted @@ -373,7 +304,7 @@ class _AthleteSessionsTab extends StatelessWidget { if (entrySnapshot.hasError) { return Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), - child: Text('Error loading entries: \${entrySnapshot.error}', + child: Text('Error loading entries: ${entrySnapshot.error}', style: theme.textTheme.bodySmall ?.copyWith(color: Colors.red)), ); @@ -443,8 +374,8 @@ class _SessionEntryTile extends StatelessWidget { ...entry.sets.asMap().entries.map((e) => Padding( padding: const EdgeInsets.only(left: 12, top: 2), child: Text( - 'Set \${e.key + 1}: \${e.value.reps} reps' - '\${e.value.weight != null ? ' @ \${e.value.weight}' : ''}', + 'Set ${e.key + 1}: ${e.value.reps} reps' + '${e.value.weight != null ? ' @ ${e.value.weight}' : ''}', style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey), ), )), @@ -476,11 +407,6 @@ class _AthleteCalendarTab extends StatelessWidget { @override Widget build(BuildContext context) { - // Reuse the athlete's calendar widget, scoped to athleteUid. - // CalendarScreen reads sessions for the currently authenticated user (coach), - // so we need a variant that reads for athleteUid. - // We inline a minimal calendar UI here since the calendar data source - // must be scoped to the athlete. return _ReadOnlyCalendar(athleteUid: athleteUid); } } @@ -526,7 +452,7 @@ class _ReadOnlyCalendarState extends State<_ReadOnlyCalendar> { .snapshots(), builder: (context, snapshot) { if (snapshot.hasError) { - return Center(child: Text('Error: \${snapshot.error}')); + return Center(child: Text('Error: ${snapshot.error}')); } if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -549,16 +475,14 @@ class _ReadOnlyCalendarState extends State<_ReadOnlyCalendar> { final selectedSessions = sessionsByDate[_selectedDate] ?? []; - // Build day cells final firstDay = DateTime(_focusedMonth.year, _focusedMonth.month, 1); final lastDay = DateTime(_focusedMonth.year, _focusedMonth.month + 1, 0); - final startWeekday = firstDay.weekday % 7; // Sunday=0 + final startWeekday = firstDay.weekday % 7; final daysInMonth = lastDay.day; final today = DateTime.now(); return Column( children: [ - // Month header Padding( padding: const EdgeInsets.all(16), child: Row( @@ -569,7 +493,7 @@ class _ReadOnlyCalendarState extends State<_ReadOnlyCalendar> { onPressed: () => _changeMonth(-1), ), Text( - '\${months[_focusedMonth.month - 1]} \${_focusedMonth.year}', + '${months[_focusedMonth.month - 1]} ${_focusedMonth.year}', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), IconButton( @@ -579,7 +503,6 @@ class _ReadOnlyCalendarState extends State<_ReadOnlyCalendar> { ], ), ), - // Weekday labels Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( @@ -589,7 +512,6 @@ class _ReadOnlyCalendarState extends State<_ReadOnlyCalendar> { ), ), const SizedBox(height: 4), - // Calendar grid Expanded( child: GridView.builder( padding: const EdgeInsets.symmetric(horizontal: 8), @@ -630,7 +552,6 @@ class _ReadOnlyCalendarState extends State<_ReadOnlyCalendar> { }, ), ), - // Selected date sessions if (selectedSessions.isNotEmpty) Expanded( child: ListView.builder( From 7cbf09fac2162245ec9cd3afb6365b2082f36645 Mon Sep 17 00:00:00 2001 From: kasionchen <33283507+Kasionchen@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:59:38 +0800 Subject: [PATCH 6/6] feat: pass readOnly to WorkoutDetailScreen in View Details When coach taps "View Details" on a WorkoutCard in athlete detail view, the WorkoutDetailScreen now receives readOnly=true to hide edit/start affordances. This completes the coach read-only viewing flow. Closes #3 --- lib/widgets/workout_card.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) ...[