Trainingsplan erstellen

main
joschy2002 2025-05-26 21:59:51 +02:00
parent cc5e51b9ec
commit b8af193bde
4 changed files with 782 additions and 358 deletions

View File

@ -5,6 +5,7 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:trainerbox/firebase_options.dart';
import 'screens/home_screen.dart';
import 'screens/login_screen.dart';
import 'screens/search_tab.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -48,6 +49,12 @@ class _MyAppState extends State<MyApp> {
home: _loggedIn
? HomeScreen(onLogoutSuccess: _handleLogoutSuccess)
: LoginScreen(onLoginSuccess: _handleLoginSuccess),
routes: {
'/search': (context) => SearchTab(
selectMode: (ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?)?['selectMode'] ?? false,
remainingTime: (ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?)?['remainingTime'] as int?,
),
},
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'training_detail_screen.dart';
class CalendarTab extends StatefulWidget {
const CalendarTab({super.key});
@ -18,6 +19,15 @@ class _CalendarTabState extends State<CalendarTab> {
bool _isLoading = false;
String? _currentUserId;
String? _userRole;
final _exerciseController = TextEditingController();
final _durationController = TextEditingController();
@override
void dispose() {
_exerciseController.dispose();
_durationController.dispose();
super.dispose();
}
@override
void initState() {
@ -52,17 +62,37 @@ class _CalendarTabState extends State<CalendarTab> {
QuerySnapshot trainersSnapshot;
if (_userRole == 'trainer') {
// Trainer sieht nur seine eigenen Trainings
trainersSnapshot = await FirebaseFirestore.instance
.collection('User')
.where('role', isEqualTo: 'trainer')
.where(FieldPath.documentId, isEqualTo: _currentUserId)
.get();
} else {
// Spieler sehen alle Trainings
final userDoc = await FirebaseFirestore.instance
.collection('User')
.doc(_currentUserId)
.get();
if (!userDoc.exists) {
setState(() => _isLoading = false);
return;
}
final userData = userDoc.data() as Map<String, dynamic>;
final userClub = userData['club'] as String?;
if (userClub == null || userClub.isEmpty) {
setState(() {
_events = {};
_isLoading = false;
});
return;
}
trainersSnapshot = await FirebaseFirestore.instance
.collection('User')
.where('role', isEqualTo: 'trainer')
.where('club', isEqualTo: userClub)
.get();
}
@ -73,6 +103,7 @@ class _CalendarTabState extends State<CalendarTab> {
final trainingTimes = trainerData['trainingTimes'] as Map<String, dynamic>? ?? {};
final trainingDurations = trainerData['trainingDurations'] as Map<String, dynamic>? ?? {};
final cancelledTrainings = trainerData['cancelledTrainings'] as List<dynamic>? ?? [];
final trainingExercises = trainerData['trainingExercises'] as Map<String, dynamic>? ?? {};
trainingTimes.forEach((day, timeStr) {
if (timeStr == null) return;
@ -88,12 +119,11 @@ class _CalendarTabState extends State<CalendarTab> {
final daysUntilNext = _getDaysUntilNext(day, now.weekday);
final eventDate = DateTime(now.year, now.month, now.day + daysUntilNext, hour, minute);
// Erstelle Trainings für ein ganzes Jahr
for (var i = 0; i < 52; i++) {
final date = eventDate.add(Duration(days: i * 7));
final normalizedDate = DateTime(date.year, date.month, date.day);
final dateString = normalizedDate.toIso8601String();
// Prüfe, ob das Training an diesem Tag abgesagt wurde
final isCancelled = cancelledTrainings.any((cancelled) {
if (cancelled is Map<String, dynamic>) {
final cancelledDate = DateTime.parse(cancelled['date'] as String);
@ -103,6 +133,14 @@ class _CalendarTabState extends State<CalendarTab> {
});
if (!isCancelled) {
final exercises = trainingExercises[dateString] as List<dynamic>? ?? [];
final totalExerciseDuration = exercises.fold<int>(0, (sum, exercise) {
if (exercise is Map<String, dynamic>) {
return sum + (exercise['duration'] as int? ?? 0);
}
return sum;
});
final event = {
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer',
'time': timeStr,
@ -110,7 +148,10 @@ class _CalendarTabState extends State<CalendarTab> {
'trainerId': trainerDoc.id,
'isCurrentUser': trainerDoc.id == _currentUserId,
'day': day,
'date': normalizedDate.toIso8601String(),
'date': dateString,
'exercises': exercises,
'remainingTime': duration - totalExerciseDuration,
'club': trainerData['club'] ?? 'Kein Verein',
};
if (events.containsKey(normalizedDate)) {
@ -133,6 +174,67 @@ class _CalendarTabState extends State<CalendarTab> {
}
}
Future<void> _addExercise(Map<String, dynamic> event) async {
if (_userRole != 'trainer' || !event['isCurrentUser']) return;
// Navigiere zum Suchbildschirm und warte auf das Ergebnis
final result = await Navigator.pushNamed(
context,
'/search',
arguments: {
'selectMode': true,
'remainingTime': event['remainingTime'],
},
);
// Wenn eine Übung ausgewählt wurde
if (result != null && result is Map<String, dynamic>) {
try {
final userDoc = await FirebaseFirestore.instance
.collection('User')
.doc(_currentUserId)
.get();
if (!userDoc.exists) return;
final data = userDoc.data() as Map<String, dynamic>;
final trainingExercises = Map<String, dynamic>.from(data['trainingExercises'] ?? {});
final exercises = List<Map<String, dynamic>>.from(trainingExercises[event['date']] ?? []);
exercises.add({
'id': result['id'],
'name': result['title'],
'description': result['description'],
'duration': result['duration'],
});
trainingExercises[event['date']] = exercises;
await FirebaseFirestore.instance
.collection('User')
.doc(_currentUserId)
.update({
'trainingExercises': trainingExercises,
});
await _loadEvents();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Übung wurde hinzugefügt')),
);
}
} catch (e) {
print('Error adding exercise: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Fehler beim Hinzufügen der Übung')),
);
}
}
}
}
Future<void> _deleteTraining(Map<String, dynamic> event) async {
if (_userRole != 'trainer' || !event['isCurrentUser']) return;
@ -147,7 +249,6 @@ class _CalendarTabState extends State<CalendarTab> {
final data = userDoc.data() as Map<String, dynamic>;
final cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []);
// Füge das Training zur Liste der abgesagten Trainings hinzu
cancelledTrainings.add({
'date': event['date'],
'day': event['day'],
@ -155,7 +256,6 @@ class _CalendarTabState extends State<CalendarTab> {
'duration': event['duration'],
});
// Aktualisiere die Daten in Firestore
await FirebaseFirestore.instance
.collection('User')
.doc(_currentUserId)
@ -163,7 +263,6 @@ class _CalendarTabState extends State<CalendarTab> {
'cancelledTrainings': cancelledTrainings,
});
// Lade die Events neu
await _loadEvents();
if (mounted) {
@ -181,6 +280,50 @@ class _CalendarTabState extends State<CalendarTab> {
}
}
Future<void> _removeExercise(Map<String, dynamic> event, Map<String, dynamic> exercise) async {
if (_userRole != 'trainer' || !event['isCurrentUser']) return;
try {
final userDoc = await FirebaseFirestore.instance
.collection('User')
.doc(_currentUserId)
.get();
if (!userDoc.exists) return;
final data = userDoc.data() as Map<String, dynamic>;
final trainingExercises = Map<String, dynamic>.from(data['trainingExercises'] ?? {});
final exercises = List<Map<String, dynamic>>.from(trainingExercises[event['date']] ?? []);
// Entferne die Übung aus der Liste
exercises.removeWhere((e) => e['id'] == exercise['id']);
trainingExercises[event['date']] = exercises;
await FirebaseFirestore.instance
.collection('User')
.doc(_currentUserId)
.update({
'trainingExercises': trainingExercises,
});
await _loadEvents();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Übung wurde entfernt')),
);
}
} catch (e) {
print('Error removing exercise: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Fehler beim Entfernen der Übung')),
);
}
}
}
int _getDaysUntilNext(String day, int currentWeekday) {
final weekdays = {
'Montag': 1,
@ -297,80 +440,183 @@ class _CalendarTabState extends State<CalendarTab> {
itemBuilder: (context, index) {
final event = _getEventsForDay(_selectedDay!)[index];
final isCurrentUser = event['isCurrentUser'] as bool;
final exercises = event['exercises'] as List<dynamic>;
final remainingTime = event['remainingTime'] as int;
return Card(
margin: const EdgeInsets.only(bottom: 8),
color: isCurrentUser ? Colors.blue.withOpacity(0.1) : null,
child: ListTile(
leading: Icon(
Icons.sports,
color: isCurrentUser ? Colors.blue : null,
),
title: Text(
isCurrentUser ? 'Training' : event['trainerName'],
style: TextStyle(
fontWeight: isCurrentUser ? FontWeight.bold : null,
),
),
subtitle: Text(
'${event['time']} - ${event['duration']} Minuten',
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.info),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(isCurrentUser ? 'Training' : event['trainerName']),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Zeit: ${event['time']}'),
const SizedBox(height: 8),
Text('Dauer: ${event['duration']} Minuten'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Schließen'),
),
],
),
);
},
child: Column(
children: [
ListTile(
leading: Icon(
Icons.sports,
color: isCurrentUser ? Colors.blue : null,
),
if (isCurrentUser)
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Training löschen'),
content: const Text('Möchten Sie dieses Training wirklich löschen?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Abbrechen'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_deleteTraining(event);
},
child: const Text('Löschen', style: TextStyle(color: Colors.red)),
),
],
),
);
},
title: Text(
isCurrentUser ? 'Training' : event['trainerName'],
style: TextStyle(
fontWeight: isCurrentUser ? FontWeight.bold : null,
),
],
),
),
subtitle: Text(
'${event['time']} - ${event['duration']} Minuten\nVerbleibende Zeit: $remainingTime Minuten',
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isCurrentUser)
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _addExercise(event),
),
IconButton(
icon: const Icon(Icons.info),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(isCurrentUser ? 'Training' : event['trainerName']),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Zeit: ${event['time']}'),
const SizedBox(height: 8),
Text('Dauer: ${event['duration']} Minuten'),
const SizedBox(height: 8),
Text('Verbleibende Zeit: $remainingTime Minuten'),
if (exercises.isNotEmpty) ...[
const SizedBox(height: 16),
const Text('Übungen:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
...exercises.map((exercise) {
if (exercise is Map<String, dynamic>) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
'${exercise['name']} - ${exercise['duration']} Minuten',
),
);
}
return const SizedBox.shrink();
}),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Schließen'),
),
],
),
);
},
),
if (isCurrentUser)
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Training löschen'),
content: const Text('Möchten Sie dieses Training wirklich löschen?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Abbrechen'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_deleteTraining(event);
},
child: const Text('Löschen', style: TextStyle(color: Colors.red)),
),
],
),
);
},
),
],
),
),
if (exercises.isNotEmpty)
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
const Text('Übungen:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
...exercises.map((exercise) {
if (exercise is Map<String, dynamic>) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
children: [
const Icon(Icons.fitness_center, size: 16),
const SizedBox(width: 8),
Expanded(
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TrainingDetailScreen(
trainingId: exercise['id'],
),
),
);
},
child: Text(
'${exercise['name']} - ${exercise['duration']} Minuten',
style: const TextStyle(
decoration: TextDecoration.underline,
color: Colors.blue,
),
),
),
),
if (isCurrentUser)
IconButton(
icon: const Icon(Icons.delete, color: Colors.red, size: 20),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Übung entfernen'),
content: const Text('Möchten Sie diese Übung wirklich entfernen?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Abbrechen'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_removeExercise(event, exercise);
},
child: const Text('Entfernen', style: TextStyle(color: Colors.red)),
),
],
),
);
},
),
],
),
);
}
return const SizedBox.shrink();
}),
],
),
),
],
),
);
},

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.dart';
class ProfileTab extends StatefulWidget {
final VoidCallback? onLogoutSuccess;
@ -14,8 +15,13 @@ class _ProfileTabState extends State<ProfileTab> {
final _formKey = GlobalKey<FormState>();
bool _isTrainer = false;
bool _isLoading = false;
Map<String, TimeOfDay> _trainingTimes = {};
Map<String, TimeOfDay?> _trainingTimes = {};
Map<String, int> _trainingDurations = {};
String _name = '';
String _email = '';
String _club = '';
String? _userRole;
DateTime? _joinDate;
@override
void initState() {
@ -27,30 +33,49 @@ class _ProfileTabState extends State<ProfileTab> {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
setState(() => _isLoading = true);
try {
final doc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get();
if (doc.exists) {
final data = doc.data()!;
setState(() {
_isTrainer = data['role'] == 'trainer';
if (_isTrainer) {
_trainingTimes = Map<String, TimeOfDay>.from(
(data['trainingTimes'] ?? {}).map(
(key, value) => MapEntry(
key,
TimeOfDay(
hour: int.parse(value.split(':')[0]),
minute: int.parse(value.split(':')[1]),
),
),
),
);
_trainingDurations = Map<String, int>.from(data['trainingDurations'] ?? {});
final trainingTimes = data['trainingTimes'] as Map<String, dynamic>? ?? {};
final trainingDurations = data['trainingDurations'] as Map<String, dynamic>? ?? {};
final convertedTrainingTimes = <String, TimeOfDay?>{};
trainingTimes.forEach((key, value) {
if (value != null) {
final timeStr = value.toString();
final timeParts = timeStr.split(':');
if (timeParts.length == 2) {
final hour = int.tryParse(timeParts[0]) ?? 0;
final minute = int.tryParse(timeParts[1]) ?? 0;
convertedTrainingTimes[key] = TimeOfDay(hour: hour, minute: minute);
}
}
});
setState(() {
_name = data['name'] ?? '';
_email = data['email'] ?? '';
_club = data['club'] ?? '';
_isTrainer = data['role'] == 'trainer';
_userRole = data['role'] as String?;
_trainingTimes = convertedTrainingTimes;
_trainingDurations = Map<String, int>.from(trainingDurations);
_joinDate = (data['createdAt'] as Timestamp?)?.toDate();
});
}
} catch (e) {
print('Error loading user data: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Laden der Daten: $e')),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@ -74,15 +99,21 @@ class _ProfileTabState extends State<ProfileTab> {
_trainingDurations[day] = duration;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Trainingszeit gespeichert')),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Trainingszeit gespeichert')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Speichern: $e')),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Speichern: $e')),
);
}
} finally {
setState(() => _isLoading = false);
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@ -102,15 +133,21 @@ class _ProfileTabState extends State<ProfileTab> {
_trainingDurations.remove(day);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Trainingszeit entfernt')),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Trainingszeit entfernt')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Entfernen: $e')),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Entfernen: $e')),
);
}
} finally {
setState(() => _isLoading = false);
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@ -132,6 +169,47 @@ class _ProfileTabState extends State<ProfileTab> {
}
}
Future<void> _saveUserData() async {
if (FirebaseAuth.instance.currentUser == null) return;
setState(() => _isLoading = true);
try {
final trainingTimesMap = <String, String>{};
_trainingTimes.forEach((key, value) {
if (value != null) {
trainingTimesMap[key] = '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}';
}
});
await FirebaseFirestore.instance
.collection('User')
.doc(FirebaseAuth.instance.currentUser!.uid)
.update({
'name': _name,
'club': _club,
'trainingTimes': trainingTimesMap,
'trainingDurations': _trainingDurations,
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Profil wurde aktualisiert')),
);
}
} catch (e) {
print('Error saving user data: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Fehler beim Speichern des Profils')),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
@ -152,92 +230,167 @@ class _ProfileTabState extends State<ProfileTab> {
}
},
),
IconButton(
icon: const Icon(Icons.save),
onPressed: _isLoading ? null : _saveUserData,
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Persönliche Informationen',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
FutureBuilder<DocumentSnapshot>(
future: FirebaseFirestore.instance.collection('User').doc(user.uid).get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || !snapshot.data!.exists) {
return const Center(child: Text('Keine Daten gefunden'));
}
final data = snapshot.data!.data() as Map<String, dynamic>;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Name: ${data['name'] ?? '-'}'),
const SizedBox(height: 8),
Text('E-Mail: ${user.email ?? '-'}'),
const SizedBox(height: 8),
Text('Rolle: ${data['role'] ?? '-'}'),
if (_isTrainer) ...[
const SizedBox(height: 24),
const Text(
'Trainingszeiten',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Persönliche Informationen',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextField(
controller: TextEditingController(text: _name),
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
onChanged: (value) => _name = value,
),
const SizedBox(height: 16),
TextField(
controller: TextEditingController(text: _email),
decoration: const InputDecoration(
labelText: 'E-Mail',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
enabled: false,
),
const SizedBox(height: 16),
TextField(
controller: TextEditingController(text: _club),
decoration: const InputDecoration(
labelText: 'Verein',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.sports),
hintText: 'Geben Sie Ihren Verein ein',
),
onChanged: (value) => _club = value,
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const Icon(Icons.work),
const SizedBox(width: 12),
Text(
'Rolle: ${_userRole == 'trainer' ? 'Trainer' : 'Spieler'}',
style: const TextStyle(fontSize: 16),
),
],
),
),
if (_joinDate != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const Icon(Icons.calendar_today),
const SizedBox(width: 12),
Text(
'Beigetreten am: ${DateFormat('dd.MM.yyyy').format(_joinDate!)}',
style: const TextStyle(fontSize: 16),
),
],
),
),
],
],
),
),
),
if (_userRole == 'trainer') ...[
const SizedBox(height: 24),
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Trainingszeiten',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
...['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
.map((day) => Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: const Icon(Icons.access_time),
title: Text(day),
subtitle: _trainingTimes[day] != null
? Text(
'${_trainingTimes[day]!.format(context)} - ${_trainingDurations[day]} Minuten',
)
: const Text('Keine Trainingszeit'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
_trainingTimes[day] != null
? Icons.edit
: Icons.add,
),
onPressed: _isLoading
? null
: () => _selectTime(context, day),
),
if (_trainingTimes[day] != null)
IconButton(
icon: const Icon(Icons.delete),
onPressed: _isLoading
? null
: () => _removeTrainingTime(day),
),
],
),
),
))
.toList(),
],
),
),
const SizedBox(height: 16),
...['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
.map((day) => Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(day),
subtitle: _trainingTimes[day] != null
? Text(
'${_trainingTimes[day]!.format(context)} - ${_trainingDurations[day]} Minuten',
)
: const Text('Keine Trainingszeit'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
_trainingTimes[day] != null
? Icons.edit
: Icons.add,
),
onPressed: _isLoading
? null
: () => _selectTime(context, day),
),
if (_trainingTimes[day] != null)
IconButton(
icon: const Icon(Icons.delete),
onPressed: _isLoading
? null
: () => _removeTrainingTime(day),
),
],
),
),
))
.toList(),
],
),
],
);
},
],
),
),
],
),
),
);
}
}

View File

@ -7,7 +7,14 @@ import 'dart:io';
import 'training_detail_screen.dart';
class SearchTab extends StatefulWidget {
const SearchTab({super.key});
final bool selectMode;
final int? remainingTime;
const SearchTab({
super.key,
this.selectMode = false,
this.remainingTime,
});
@override
State<SearchTab> createState() => _SearchTabState();
@ -63,7 +70,7 @@ class _SearchTabState extends State<SearchTab> {
}
}
void _showCreateTrainingDialog() {
void _showCreateTrainingDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => _CreateTrainingDialog(categories: _categories),
@ -88,191 +95,202 @@ class _SearchTabState extends State<SearchTab> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
title: TextField(
appBar: AppBar(
title: const Text('Übungen'),
actions: [
if (widget.selectMode)
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
if (_isTrainer && !widget.selectMode)
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showCreateTrainingDialog(context),
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Suche nach Training...',
border: InputBorder.none,
hintText: 'Suche nach Übungen...',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () => _searchController.clear(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
actions: [
if (_trainerChecked && _isTrainer)
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Neues Training erstellen',
onPressed: _showCreateTrainingDialog,
),
],
),
SliverPadding(
if (widget.selectMode && widget.remainingTime != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'Verbleibende Zeit: ${widget.remainingTime} Minuten',
style: const TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
sliver: SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Kategorien',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: _categories.map((category) {
return FilterChip(
label: Text(category),
selected: _selectedCategory == category,
onSelected: (bool selected) {
setState(() {
_selectedCategory = selected ? category : null;
});
},
);
}).toList(),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Kategorien',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: _categories.map((category) {
return FilterChip(
label: Text(category),
selected: _selectedCategory == category,
onSelected: (bool selected) {
setState(() {
_selectedCategory = selected ? category : null;
});
},
);
}).toList(),
),
],
),
),
if (_searchTerm.isNotEmpty || _selectedCategory != null)
SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: FutureBuilder<QuerySnapshot>(
future: FirebaseFirestore.instance.collection('Training').get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SliverToBoxAdapter(
child: Center(child: CircularProgressIndicator()));
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return const SliverToBoxAdapter(
child: Center(child: Text('Keine Trainings gefunden.')));
}
final docs = snapshot.data!.docs.where((doc) {
Expanded(
child: FutureBuilder<QuerySnapshot>(
future: FirebaseFirestore.instance.collection('Training').get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Fehler: ${snapshot.error}'));
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return const Center(child: Text('Keine Übungen gefunden'));
}
var exercises = snapshot.data!.docs.where((doc) {
final data = doc.data() as Map<String, dynamic>;
final title = data['title']?.toString().toLowerCase() ?? '';
final description = data['description']?.toString().toLowerCase() ?? '';
final category = data['category']?.toString() ?? '';
final searchTerm = _searchTerm.toLowerCase();
final matchesSearch = title.contains(searchTerm) || description.contains(searchTerm);
final matchesCategory = _selectedCategory == null || category == _selectedCategory;
return matchesSearch && matchesCategory;
}).toList();
if (exercises.isEmpty) {
return const Center(child: Text('Keine Übungen gefunden'));
}
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.75,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: exercises.length,
itemBuilder: (context, index) {
final doc = exercises[index];
final data = doc.data() as Map<String, dynamic>;
final title = (data['title'] ?? '').toString().toLowerCase();
final description = (data['description'] ?? '').toString().toLowerCase();
final category = (data['category'] ?? '').toString();
final matchesSearch = _searchTerm.isEmpty ||
title.contains(_searchTerm.toLowerCase()) ||
description.contains(_searchTerm.toLowerCase());
final matchesCategory = _selectedCategory == null || category == _selectedCategory;
return matchesSearch && matchesCategory;
}).toList();
if (docs.isEmpty) {
return const SliverToBoxAdapter(
child: Center(child: Text('Keine Trainings gefunden.')));
}
return SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 0.75,
),
delegate: SliverChildBuilderDelegate((context, index) {
final data = docs[index].data() as Map<String, dynamic>;
final isFavorite = _favorites.contains(docs[index].id);
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TrainingDetailScreen(trainingId: docs[index].id),
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: (data['picture'] is String && data['picture'] != '')
? Image.network(
data['picture'],
width: double.infinity,
fit: BoxFit.cover,
)
: Container(
color: Colors.grey[300],
child: const Center(
child: Icon(Icons.fitness_center, size: 40),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data['title'] ?? '-',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
final duration = (data['duration'] as num?)?.toInt() ?? 0;
final isDisabled = widget.selectMode && duration > (widget.remainingTime ?? 0);
return Card(
child: InkWell(
onTap: isDisabled
? null
: () {
if (widget.selectMode) {
Navigator.pop(context, {
'id': doc.id,
'title': data['title']?.toString() ?? 'Unbekannte Übung',
'description': data['description']?.toString() ?? '',
'duration': duration,
});
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TrainingDetailScreen(trainingId: doc.id),
),
const SizedBox(height: 4),
Text(
data['description'] ?? '-',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.grey[600],
fontSize: 13,
),
),
const SizedBox(height: 4),
Text(
'${data['duration'] ?? '-'} Minuten',
style: TextStyle(
color: Colors.grey[600],
fontSize: 13,
),
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.star, size: 16, color: Colors.amber),
const SizedBox(width: 4),
Text('${data['rating overall'] ?? '-'}'),
],
),
const SizedBox(height: 4),
Text('Level: ${data['year'] ?? '-'}'),
const SizedBox(height: 4),
Align(
alignment: Alignment.bottomRight,
child: IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : null,
),
onPressed: () => _toggleFavorite(docs[index].id, isFavorite),
),
),
],
);
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Container(
color: Colors.grey[200],
child: const Center(
child: Icon(Icons.fitness_center, size: 50),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data['title']?.toString() ?? 'Unbekannte Übung',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'${data['description']?.toString() ?? ''}\nDauer: $duration Minuten',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
if (isDisabled)
const Padding(
padding: EdgeInsets.only(top: 4),
child: Text(
'Passt nicht in die verbleibende Zeit',
style: TextStyle(
color: Colors.orange,
fontSize: 12,
),
),
),
],
),
),
],
),
);
}, childCount: docs.length),
);
},
),
),
);
},
);
},
),
),
],
),
);