Files
trainhub-flutter/lib/presentation/calendar/widgets/program_week_view.dart
Kazimierz Ciołek 0c9eb8878d
Some checks failed
Build Linux App / build (push) Failing after 1m33s
Refactoring
2026-02-23 10:02:23 -05:00

465 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/core/utils/id_generator.dart';
import 'package:trainhub_flutter/domain/entities/program_week.dart';
import 'package:trainhub_flutter/domain/entities/program_workout.dart';
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
class ProgramWeekView extends StatelessWidget {
final ProgramWeekEntity week;
final List<ProgramWorkoutEntity> workouts;
final List<TrainingPlanEntity> availablePlans;
final Function(ProgramWorkoutEntity) onAddWorkout;
final Function(String) onDeleteWorkout;
const ProgramWeekView({
super.key,
required this.week,
required this.workouts,
required this.availablePlans,
required this.onAddWorkout,
required this.onDeleteWorkout,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 24),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Week ${week.position}',
style: Theme.of(context).textTheme.headlineSmall,
),
const Divider(),
SizedBox(
height: 500,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: List.generate(7, (dayIndex) {
final dayNum = dayIndex + 1;
final dayWorkouts = workouts
.where((w) => w.day == dayNum.toString())
.toList();
return Expanded(
child: _DayColumn(
dayNum: dayNum,
dayIndex: dayIndex,
dayWorkouts: dayWorkouts,
availablePlans: availablePlans,
week: week,
onAddWorkout: onAddWorkout,
onDeleteWorkout: onDeleteWorkout,
),
);
}),
),
),
],
),
),
);
}
}
class _DayColumn extends StatelessWidget {
final int dayNum;
final int dayIndex;
final List<ProgramWorkoutEntity> dayWorkouts;
final List<TrainingPlanEntity> availablePlans;
final ProgramWeekEntity week;
final Function(ProgramWorkoutEntity) onAddWorkout;
final Function(String) onDeleteWorkout;
const _DayColumn({
required this.dayNum,
required this.dayIndex,
required this.dayWorkouts,
required this.availablePlans,
required this.week,
required this.onAddWorkout,
required this.onDeleteWorkout,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: dayIndex < 6
? const Border(
right: BorderSide(color: Colors.grey, width: 0.5),
)
: null,
color: dayIndex % 2 == 0
? Theme.of(context).colorScheme.surfaceContainerLow
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
border: const Border(
bottom: BorderSide(color: Colors.grey, width: 0.5),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_getDayName(dayNum),
style: const TextStyle(fontWeight: FontWeight.bold),
),
InkWell(
onTap: () => _showAddWorkoutSheet(context),
borderRadius: BorderRadius.circular(16),
child: const Icon(Icons.add_circle_outline, size: 20),
),
],
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(4),
child: Column(
children: [
if (dayWorkouts.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Center(
child: Text(
'Rest',
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
fontSize: 12,
),
),
),
)
else
...dayWorkouts.map(
(workout) => _WorkoutCard(
workout: workout,
onDelete: () => onDeleteWorkout(workout.id),
),
),
],
),
),
),
],
),
);
}
String _getDayName(int day) {
const days = [
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
'Sun',
];
if (day >= 1 && day <= 7) return days[day - 1];
return 'Day $day';
}
void _showAddWorkoutSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => _AddWorkoutSheet(
availablePlans: availablePlans,
week: week,
dayNum: dayNum,
onAddWorkout: (workout) {
onAddWorkout(workout);
Navigator.pop(context);
},
),
);
}
}
class _WorkoutCard extends StatelessWidget {
final ProgramWorkoutEntity workout;
final VoidCallback onDelete;
const _WorkoutCard({required this.workout, required this.onDelete});
@override
Widget build(BuildContext context) {
final isNote = workout.type == 'note';
return Card(
margin: const EdgeInsets.only(bottom: 4),
color: isNote
? AppColors.info.withValues(alpha: 0.08)
: null,
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 0,
),
visualDensity: VisualDensity.compact,
leading: Icon(
isNote ? Icons.sticky_note_2_outlined : Icons.fitness_center,
size: 14,
color: isNote ? AppColors.info : AppColors.textMuted,
),
title: Text(
workout.name ?? 'Untitled',
style: const TextStyle(fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: workout.description != null && workout.description!.isNotEmpty
? Text(
workout.description!,
style: const TextStyle(fontSize: 10),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: null,
trailing: InkWell(
onTap: onDelete,
borderRadius: BorderRadius.circular(12),
child: const Icon(Icons.close, size: 14),
),
),
);
}
}
class _AddWorkoutSheet extends StatefulWidget {
final List<TrainingPlanEntity> availablePlans;
final ProgramWeekEntity week;
final int dayNum;
final Function(ProgramWorkoutEntity) onAddWorkout;
const _AddWorkoutSheet({
required this.availablePlans,
required this.week,
required this.dayNum,
required this.onAddWorkout,
});
@override
State<_AddWorkoutSheet> createState() => _AddWorkoutSheetState();
}
class _AddWorkoutSheetState extends State<_AddWorkoutSheet>
with SingleTickerProviderStateMixin {
late final TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.6,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Text(
'Add to Schedule',
style: Theme.of(context).textTheme.titleMedium,
),
),
TabBar(
controller: _tabController,
tabs: const [
Tab(
icon: Icon(Icons.fitness_center, size: 16),
text: 'Training Plan',
),
Tab(
icon: Icon(Icons.sticky_note_2_outlined, size: 16),
text: 'Note',
),
],
),
const Divider(height: 1),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_PlanPickerTab(
availablePlans: widget.availablePlans,
week: widget.week,
dayNum: widget.dayNum,
onAddWorkout: widget.onAddWorkout,
),
_NoteTab(
week: widget.week,
dayNum: widget.dayNum,
onAddWorkout: widget.onAddWorkout,
),
],
),
),
],
),
);
}
}
class _PlanPickerTab extends StatelessWidget {
final List<TrainingPlanEntity> availablePlans;
final ProgramWeekEntity week;
final int dayNum;
final Function(ProgramWorkoutEntity) onAddWorkout;
const _PlanPickerTab({
required this.availablePlans,
required this.week,
required this.dayNum,
required this.onAddWorkout,
});
@override
Widget build(BuildContext context) {
if (availablePlans.isEmpty) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text('No training plans available. Create one first!'),
),
);
}
return ListView.builder(
itemCount: availablePlans.length,
itemBuilder: (context, index) {
final plan = availablePlans[index];
return ListTile(
leading: const Icon(Icons.fitness_center),
title: Text(plan.name),
subtitle: Text('${plan.totalExercises} exercises'),
onTap: () {
final newWorkout = ProgramWorkoutEntity(
id: IdGenerator.generate(),
programId: week.programId,
weekId: week.id,
day: dayNum.toString(),
type: 'workout',
name: plan.name,
refId: plan.id,
description: '${plan.sections.length} sections',
completed: false,
);
onAddWorkout(newWorkout);
},
);
},
);
}
}
class _NoteTab extends StatefulWidget {
final ProgramWeekEntity week;
final int dayNum;
final Function(ProgramWorkoutEntity) onAddWorkout;
const _NoteTab({
required this.week,
required this.dayNum,
required this.onAddWorkout,
});
@override
State<_NoteTab> createState() => _NoteTabState();
}
class _NoteTabState extends State<_NoteTab> {
final TextEditingController _titleController = TextEditingController();
final TextEditingController _contentController = TextEditingController();
@override
void dispose() {
_titleController.dispose();
_contentController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(UIConstants.spacing16),
child: Column(
children: [
TextField(
controller: _titleController,
decoration: const InputDecoration(
labelText: 'Title',
hintText: 'e.g. Active Recovery',
),
autofocus: true,
),
const SizedBox(height: UIConstants.spacing12),
TextField(
controller: _contentController,
decoration: const InputDecoration(
labelText: 'Note (optional)',
hintText: 'Additional details...',
),
maxLines: 3,
),
const SizedBox(height: UIConstants.spacing16),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () {
if (_titleController.text.isEmpty) return;
final newWorkout = ProgramWorkoutEntity(
id: IdGenerator.generate(),
programId: widget.week.programId,
weekId: widget.week.id,
day: widget.dayNum.toString(),
type: 'note',
name: _titleController.text,
description: _contentController.text.isEmpty
? null
: _contentController.text,
completed: false,
);
widget.onAddWorkout(newWorkout);
},
icon: const Icon(Icons.add),
label: const Text('Add Note'),
),
),
],
),
);
}
}