Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 196 additions & 71 deletions lib/screens/athlete_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
Expand All @@ -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'),
],
),
),
Expand All @@ -37,6 +39,7 @@ class AthleteDetailScreen extends StatelessWidget {
_AthleteProgramsTab(athleteUid: athleteUid),
_AthleteWorkoutsTab(athleteUid: athleteUid),
_AthleteSessionsTab(athleteUid: athleteUid),
_AthleteCalendarTab(athleteUid: athleteUid),
],
),
),
Expand Down Expand Up @@ -135,7 +138,10 @@ class _ProgramTile extends StatelessWidget {
children: docs.map((doc) {
final workout = Workout.fromMap(
doc.id, doc.data() as Map<String, dynamic>);
return _WorkoutExpansionTile(workout: workout);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: WorkoutCard(workout: workout, readOnly: true),
);
}).toList(),
);
},
Expand All @@ -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});
Expand Down Expand Up @@ -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<QuerySnapshot>(
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<String, dynamic>,
);
}).toList();

final sessionsByDate = <DateTime, List<WorkoutSession>>{};
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),
),
),
],
);
},
);
}
}
33 changes: 18 additions & 15 deletions lib/screens/workout_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<WorkoutDetailScreen> createState() => _WorkoutDetailScreenState();
Expand All @@ -34,13 +35,14 @@ class _WorkoutDetailScreenState extends State<WorkoutDetailScreen> {
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(
Expand All @@ -65,16 +67,17 @@ class _WorkoutDetailScreenState extends State<WorkoutDetailScreen> {
),
],
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) ...[
Expand Down
Loading
Loading