Trainingsplan erstellen
parent
cc5e51b9ec
commit
b8af193bde
|
|
@ -5,6 +5,7 @@ import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:trainerbox/firebase_options.dart';
|
import 'package:trainerbox/firebase_options.dart';
|
||||||
import 'screens/home_screen.dart';
|
import 'screens/home_screen.dart';
|
||||||
import 'screens/login_screen.dart';
|
import 'screens/login_screen.dart';
|
||||||
|
import 'screens/search_tab.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
@ -48,6 +49,12 @@ class _MyAppState extends State<MyApp> {
|
||||||
home: _loggedIn
|
home: _loggedIn
|
||||||
? HomeScreen(onLogoutSuccess: _handleLogoutSuccess)
|
? HomeScreen(onLogoutSuccess: _handleLogoutSuccess)
|
||||||
: LoginScreen(onLoginSuccess: _handleLoginSuccess),
|
: 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?,
|
||||||
|
),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:table_calendar/table_calendar.dart';
|
import 'package:table_calendar/table_calendar.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'training_detail_screen.dart';
|
||||||
|
|
||||||
class CalendarTab extends StatefulWidget {
|
class CalendarTab extends StatefulWidget {
|
||||||
const CalendarTab({super.key});
|
const CalendarTab({super.key});
|
||||||
|
|
@ -18,6 +19,15 @@ class _CalendarTabState extends State<CalendarTab> {
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _currentUserId;
|
String? _currentUserId;
|
||||||
String? _userRole;
|
String? _userRole;
|
||||||
|
final _exerciseController = TextEditingController();
|
||||||
|
final _durationController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_exerciseController.dispose();
|
||||||
|
_durationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -52,17 +62,37 @@ class _CalendarTabState extends State<CalendarTab> {
|
||||||
QuerySnapshot trainersSnapshot;
|
QuerySnapshot trainersSnapshot;
|
||||||
|
|
||||||
if (_userRole == 'trainer') {
|
if (_userRole == 'trainer') {
|
||||||
// Trainer sieht nur seine eigenen Trainings
|
|
||||||
trainersSnapshot = await FirebaseFirestore.instance
|
trainersSnapshot = await FirebaseFirestore.instance
|
||||||
.collection('User')
|
.collection('User')
|
||||||
.where('role', isEqualTo: 'trainer')
|
.where('role', isEqualTo: 'trainer')
|
||||||
.where(FieldPath.documentId, isEqualTo: _currentUserId)
|
.where(FieldPath.documentId, isEqualTo: _currentUserId)
|
||||||
.get();
|
.get();
|
||||||
} else {
|
} 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
|
trainersSnapshot = await FirebaseFirestore.instance
|
||||||
.collection('User')
|
.collection('User')
|
||||||
.where('role', isEqualTo: 'trainer')
|
.where('role', isEqualTo: 'trainer')
|
||||||
|
.where('club', isEqualTo: userClub)
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,6 +103,7 @@ class _CalendarTabState extends State<CalendarTab> {
|
||||||
final trainingTimes = trainerData['trainingTimes'] as Map<String, dynamic>? ?? {};
|
final trainingTimes = trainerData['trainingTimes'] as Map<String, dynamic>? ?? {};
|
||||||
final trainingDurations = trainerData['trainingDurations'] as Map<String, dynamic>? ?? {};
|
final trainingDurations = trainerData['trainingDurations'] as Map<String, dynamic>? ?? {};
|
||||||
final cancelledTrainings = trainerData['cancelledTrainings'] as List<dynamic>? ?? [];
|
final cancelledTrainings = trainerData['cancelledTrainings'] as List<dynamic>? ?? [];
|
||||||
|
final trainingExercises = trainerData['trainingExercises'] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
trainingTimes.forEach((day, timeStr) {
|
trainingTimes.forEach((day, timeStr) {
|
||||||
if (timeStr == null) return;
|
if (timeStr == null) return;
|
||||||
|
|
@ -88,12 +119,11 @@ class _CalendarTabState extends State<CalendarTab> {
|
||||||
final daysUntilNext = _getDaysUntilNext(day, now.weekday);
|
final daysUntilNext = _getDaysUntilNext(day, now.weekday);
|
||||||
final eventDate = DateTime(now.year, now.month, now.day + daysUntilNext, hour, minute);
|
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++) {
|
for (var i = 0; i < 52; i++) {
|
||||||
final date = eventDate.add(Duration(days: i * 7));
|
final date = eventDate.add(Duration(days: i * 7));
|
||||||
final normalizedDate = DateTime(date.year, date.month, date.day);
|
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) {
|
final isCancelled = cancelledTrainings.any((cancelled) {
|
||||||
if (cancelled is Map<String, dynamic>) {
|
if (cancelled is Map<String, dynamic>) {
|
||||||
final cancelledDate = DateTime.parse(cancelled['date'] as String);
|
final cancelledDate = DateTime.parse(cancelled['date'] as String);
|
||||||
|
|
@ -103,6 +133,14 @@ class _CalendarTabState extends State<CalendarTab> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isCancelled) {
|
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 = {
|
final event = {
|
||||||
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer',
|
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer',
|
||||||
'time': timeStr,
|
'time': timeStr,
|
||||||
|
|
@ -110,7 +148,10 @@ class _CalendarTabState extends State<CalendarTab> {
|
||||||
'trainerId': trainerDoc.id,
|
'trainerId': trainerDoc.id,
|
||||||
'isCurrentUser': trainerDoc.id == _currentUserId,
|
'isCurrentUser': trainerDoc.id == _currentUserId,
|
||||||
'day': day,
|
'day': day,
|
||||||
'date': normalizedDate.toIso8601String(),
|
'date': dateString,
|
||||||
|
'exercises': exercises,
|
||||||
|
'remainingTime': duration - totalExerciseDuration,
|
||||||
|
'club': trainerData['club'] ?? 'Kein Verein',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (events.containsKey(normalizedDate)) {
|
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 {
|
Future<void> _deleteTraining(Map<String, dynamic> event) async {
|
||||||
if (_userRole != 'trainer' || !event['isCurrentUser']) return;
|
if (_userRole != 'trainer' || !event['isCurrentUser']) return;
|
||||||
|
|
||||||
|
|
@ -147,7 +249,6 @@ class _CalendarTabState extends State<CalendarTab> {
|
||||||
final data = userDoc.data() as Map<String, dynamic>;
|
final data = userDoc.data() as Map<String, dynamic>;
|
||||||
final cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []);
|
final cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []);
|
||||||
|
|
||||||
// Füge das Training zur Liste der abgesagten Trainings hinzu
|
|
||||||
cancelledTrainings.add({
|
cancelledTrainings.add({
|
||||||
'date': event['date'],
|
'date': event['date'],
|
||||||
'day': event['day'],
|
'day': event['day'],
|
||||||
|
|
@ -155,7 +256,6 @@ class _CalendarTabState extends State<CalendarTab> {
|
||||||
'duration': event['duration'],
|
'duration': event['duration'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Aktualisiere die Daten in Firestore
|
|
||||||
await FirebaseFirestore.instance
|
await FirebaseFirestore.instance
|
||||||
.collection('User')
|
.collection('User')
|
||||||
.doc(_currentUserId)
|
.doc(_currentUserId)
|
||||||
|
|
@ -163,7 +263,6 @@ class _CalendarTabState extends State<CalendarTab> {
|
||||||
'cancelledTrainings': cancelledTrainings,
|
'cancelledTrainings': cancelledTrainings,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Lade die Events neu
|
|
||||||
await _loadEvents();
|
await _loadEvents();
|
||||||
|
|
||||||
if (mounted) {
|
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) {
|
int _getDaysUntilNext(String day, int currentWeekday) {
|
||||||
final weekdays = {
|
final weekdays = {
|
||||||
'Montag': 1,
|
'Montag': 1,
|
||||||
|
|
@ -297,80 +440,183 @@ class _CalendarTabState extends State<CalendarTab> {
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final event = _getEventsForDay(_selectedDay!)[index];
|
final event = _getEventsForDay(_selectedDay!)[index];
|
||||||
final isCurrentUser = event['isCurrentUser'] as bool;
|
final isCurrentUser = event['isCurrentUser'] as bool;
|
||||||
|
final exercises = event['exercises'] as List<dynamic>;
|
||||||
|
final remainingTime = event['remainingTime'] as int;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
color: isCurrentUser ? Colors.blue.withOpacity(0.1) : null,
|
color: isCurrentUser ? Colors.blue.withOpacity(0.1) : null,
|
||||||
child: ListTile(
|
child: Column(
|
||||||
leading: Icon(
|
children: [
|
||||||
Icons.sports,
|
ListTile(
|
||||||
color: isCurrentUser ? Colors.blue : null,
|
leading: Icon(
|
||||||
),
|
Icons.sports,
|
||||||
title: Text(
|
color: isCurrentUser ? Colors.blue : null,
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
if (isCurrentUser)
|
title: Text(
|
||||||
IconButton(
|
isCurrentUser ? 'Training' : event['trainerName'],
|
||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
style: TextStyle(
|
||||||
onPressed: () {
|
fontWeight: isCurrentUser ? FontWeight.bold : null,
|
||||||
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)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
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();
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class ProfileTab extends StatefulWidget {
|
class ProfileTab extends StatefulWidget {
|
||||||
final VoidCallback? onLogoutSuccess;
|
final VoidCallback? onLogoutSuccess;
|
||||||
|
|
@ -14,8 +15,13 @@ class _ProfileTabState extends State<ProfileTab> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool _isTrainer = false;
|
bool _isTrainer = false;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
Map<String, TimeOfDay> _trainingTimes = {};
|
Map<String, TimeOfDay?> _trainingTimes = {};
|
||||||
Map<String, int> _trainingDurations = {};
|
Map<String, int> _trainingDurations = {};
|
||||||
|
String _name = '';
|
||||||
|
String _email = '';
|
||||||
|
String _club = '';
|
||||||
|
String? _userRole;
|
||||||
|
DateTime? _joinDate;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -27,30 +33,49 @@ class _ProfileTabState extends State<ProfileTab> {
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
|
|
||||||
|
setState(() => _isLoading = true);
|
||||||
try {
|
try {
|
||||||
final doc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get();
|
final doc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get();
|
||||||
if (doc.exists) {
|
if (doc.exists) {
|
||||||
final data = doc.data()!;
|
final data = doc.data()!;
|
||||||
setState(() {
|
final trainingTimes = data['trainingTimes'] as Map<String, dynamic>? ?? {};
|
||||||
_isTrainer = data['role'] == 'trainer';
|
final trainingDurations = data['trainingDurations'] as Map<String, dynamic>? ?? {};
|
||||||
if (_isTrainer) {
|
|
||||||
_trainingTimes = Map<String, TimeOfDay>.from(
|
final convertedTrainingTimes = <String, TimeOfDay?>{};
|
||||||
(data['trainingTimes'] ?? {}).map(
|
trainingTimes.forEach((key, value) {
|
||||||
(key, value) => MapEntry(
|
if (value != null) {
|
||||||
key,
|
final timeStr = value.toString();
|
||||||
TimeOfDay(
|
final timeParts = timeStr.split(':');
|
||||||
hour: int.parse(value.split(':')[0]),
|
if (timeParts.length == 2) {
|
||||||
minute: int.parse(value.split(':')[1]),
|
final hour = int.tryParse(timeParts[0]) ?? 0;
|
||||||
),
|
final minute = int.tryParse(timeParts[1]) ?? 0;
|
||||||
),
|
convertedTrainingTimes[key] = TimeOfDay(hour: hour, minute: minute);
|
||||||
),
|
}
|
||||||
);
|
|
||||||
_trainingDurations = Map<String, int>.from(data['trainingDurations'] ?? {});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) {
|
} catch (e) {
|
||||||
print('Error loading user data: $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;
|
_trainingDurations[day] = duration;
|
||||||
});
|
});
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
const SnackBar(content: Text('Trainingszeit gespeichert')),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
);
|
const SnackBar(content: Text('Trainingszeit gespeichert')),
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
SnackBar(content: Text('Fehler beim Speichern: $e')),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
);
|
SnackBar(content: Text('Fehler beim Speichern: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isLoading = false);
|
if (mounted) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,15 +133,21 @@ class _ProfileTabState extends State<ProfileTab> {
|
||||||
_trainingDurations.remove(day);
|
_trainingDurations.remove(day);
|
||||||
});
|
});
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
const SnackBar(content: Text('Trainingszeit entfernt')),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
);
|
const SnackBar(content: Text('Trainingszeit entfernt')),
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
SnackBar(content: Text('Fehler beim Entfernen: $e')),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
);
|
SnackBar(content: Text('Fehler beim Entfernen: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
} finally {
|
} 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
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(
|
body: _isLoading
|
||||||
padding: const EdgeInsets.all(16.0),
|
? const Center(child: CircularProgressIndicator())
|
||||||
child: Column(
|
: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.all(16.0),
|
||||||
children: [
|
child: Column(
|
||||||
const Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
'Persönliche Informationen',
|
children: [
|
||||||
style: TextStyle(
|
Card(
|
||||||
fontSize: 20,
|
elevation: 4,
|
||||||
fontWeight: FontWeight.bold,
|
child: Padding(
|
||||||
),
|
padding: const EdgeInsets.all(16.0),
|
||||||
),
|
child: Column(
|
||||||
const SizedBox(height: 16),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
FutureBuilder<DocumentSnapshot>(
|
children: [
|
||||||
future: FirebaseFirestore.instance.collection('User').doc(user.uid).get(),
|
const Text(
|
||||||
builder: (context, snapshot) {
|
'Persönliche Informationen',
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
style: TextStyle(
|
||||||
return const Center(child: CircularProgressIndicator());
|
fontSize: 20,
|
||||||
}
|
fontWeight: FontWeight.bold,
|
||||||
if (!snapshot.hasData || !snapshot.data!.exists) {
|
),
|
||||||
return const Center(child: Text('Keine Daten gefunden'));
|
),
|
||||||
}
|
const SizedBox(height: 16),
|
||||||
final data = snapshot.data!.data() as Map<String, dynamic>;
|
TextField(
|
||||||
return Column(
|
controller: TextEditingController(text: _name),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
decoration: const InputDecoration(
|
||||||
children: [
|
labelText: 'Name',
|
||||||
Text('Name: ${data['name'] ?? '-'}'),
|
border: OutlineInputBorder(),
|
||||||
const SizedBox(height: 8),
|
prefixIcon: Icon(Icons.person),
|
||||||
Text('E-Mail: ${user.email ?? '-'}'),
|
),
|
||||||
const SizedBox(height: 8),
|
onChanged: (value) => _name = value,
|
||||||
Text('Rolle: ${data['role'] ?? '-'}'),
|
),
|
||||||
if (_isTrainer) ...[
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 24),
|
TextField(
|
||||||
const Text(
|
controller: TextEditingController(text: _email),
|
||||||
'Trainingszeiten',
|
decoration: const InputDecoration(
|
||||||
style: TextStyle(
|
labelText: 'E-Mail',
|
||||||
fontSize: 18,
|
border: OutlineInputBorder(),
|
||||||
fontWeight: FontWeight.bold,
|
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(),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,14 @@ import 'dart:io';
|
||||||
import 'training_detail_screen.dart';
|
import 'training_detail_screen.dart';
|
||||||
|
|
||||||
class SearchTab extends StatefulWidget {
|
class SearchTab extends StatefulWidget {
|
||||||
const SearchTab({super.key});
|
final bool selectMode;
|
||||||
|
final int? remainingTime;
|
||||||
|
|
||||||
|
const SearchTab({
|
||||||
|
super.key,
|
||||||
|
this.selectMode = false,
|
||||||
|
this.remainingTime,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SearchTab> createState() => _SearchTabState();
|
State<SearchTab> createState() => _SearchTabState();
|
||||||
|
|
@ -63,7 +70,7 @@ class _SearchTabState extends State<SearchTab> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showCreateTrainingDialog() {
|
void _showCreateTrainingDialog(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _CreateTrainingDialog(categories: _categories),
|
builder: (context) => _CreateTrainingDialog(categories: _categories),
|
||||||
|
|
@ -88,191 +95,202 @@ class _SearchTabState extends State<SearchTab> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: CustomScrollView(
|
appBar: AppBar(
|
||||||
slivers: [
|
title: const Text('Übungen'),
|
||||||
SliverAppBar(
|
actions: [
|
||||||
floating: true,
|
if (widget.selectMode)
|
||||||
title: TextField(
|
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,
|
controller: _searchController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Suche nach Training...',
|
hintText: 'Suche nach Übungen...',
|
||||||
border: InputBorder.none,
|
|
||||||
prefixIcon: const Icon(Icons.search),
|
prefixIcon: const Icon(Icons.search),
|
||||||
suffixIcon: IconButton(
|
border: OutlineInputBorder(
|
||||||
icon: const Icon(Icons.clear),
|
borderRadius: BorderRadius.circular(10),
|
||||||
onPressed: () => _searchController.clear(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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),
|
padding: const EdgeInsets.all(16.0),
|
||||||
sliver: SliverToBoxAdapter(
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
const Text(
|
||||||
const Text(
|
'Kategorien',
|
||||||
'Kategorien',
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 16),
|
Wrap(
|
||||||
Wrap(
|
spacing: 8,
|
||||||
spacing: 8,
|
runSpacing: 8,
|
||||||
runSpacing: 8,
|
children: _categories.map((category) {
|
||||||
children: _categories.map((category) {
|
return FilterChip(
|
||||||
return FilterChip(
|
label: Text(category),
|
||||||
label: Text(category),
|
selected: _selectedCategory == category,
|
||||||
selected: _selectedCategory == category,
|
onSelected: (bool selected) {
|
||||||
onSelected: (bool selected) {
|
setState(() {
|
||||||
setState(() {
|
_selectedCategory = selected ? category : null;
|
||||||
_selectedCategory = selected ? category : null;
|
});
|
||||||
});
|
},
|
||||||
},
|
);
|
||||||
);
|
}).toList(),
|
||||||
}).toList(),
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_searchTerm.isNotEmpty || _selectedCategory != null)
|
Expanded(
|
||||||
SliverPadding(
|
child: FutureBuilder<QuerySnapshot>(
|
||||||
padding: const EdgeInsets.all(16.0),
|
future: FirebaseFirestore.instance.collection('Training').get(),
|
||||||
sliver: FutureBuilder<QuerySnapshot>(
|
builder: (context, snapshot) {
|
||||||
future: FirebaseFirestore.instance.collection('Training').get(),
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
builder: (context, snapshot) {
|
return const Center(child: CircularProgressIndicator());
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
}
|
||||||
return const SliverToBoxAdapter(
|
|
||||||
child: Center(child: CircularProgressIndicator()));
|
if (snapshot.hasError) {
|
||||||
}
|
return Center(child: Text('Fehler: ${snapshot.error}'));
|
||||||
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
|
}
|
||||||
return const SliverToBoxAdapter(
|
|
||||||
child: Center(child: Text('Keine Trainings gefunden.')));
|
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
|
||||||
}
|
return const Center(child: Text('Keine Übungen gefunden'));
|
||||||
final docs = snapshot.data!.docs.where((doc) {
|
}
|
||||||
|
|
||||||
|
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 data = doc.data() as Map<String, dynamic>;
|
||||||
final title = (data['title'] ?? '').toString().toLowerCase();
|
final duration = (data['duration'] as num?)?.toInt() ?? 0;
|
||||||
final description = (data['description'] ?? '').toString().toLowerCase();
|
final isDisabled = widget.selectMode && duration > (widget.remainingTime ?? 0);
|
||||||
final category = (data['category'] ?? '').toString();
|
|
||||||
final matchesSearch = _searchTerm.isEmpty ||
|
return Card(
|
||||||
title.contains(_searchTerm.toLowerCase()) ||
|
child: InkWell(
|
||||||
description.contains(_searchTerm.toLowerCase());
|
onTap: isDisabled
|
||||||
final matchesCategory = _selectedCategory == null || category == _selectedCategory;
|
? null
|
||||||
return matchesSearch && matchesCategory;
|
: () {
|
||||||
}).toList();
|
if (widget.selectMode) {
|
||||||
if (docs.isEmpty) {
|
Navigator.pop(context, {
|
||||||
return const SliverToBoxAdapter(
|
'id': doc.id,
|
||||||
child: Center(child: Text('Keine Trainings gefunden.')));
|
'title': data['title']?.toString() ?? 'Unbekannte Übung',
|
||||||
}
|
'description': data['description']?.toString() ?? '',
|
||||||
return SliverGrid(
|
'duration': duration,
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
});
|
||||||
crossAxisCount: 2,
|
} else {
|
||||||
mainAxisSpacing: 16,
|
Navigator.push(
|
||||||
crossAxisSpacing: 16,
|
context,
|
||||||
childAspectRatio: 0.75,
|
MaterialPageRoute(
|
||||||
),
|
builder: (context) => TrainingDetailScreen(trainingId: doc.id),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
);
|
||||||
Text(
|
}
|
||||||
data['description'] ?? '-',
|
},
|
||||||
maxLines: 2,
|
child: Column(
|
||||||
overflow: TextOverflow.ellipsis,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
style: TextStyle(
|
children: [
|
||||||
color: Colors.grey[600],
|
Expanded(
|
||||||
fontSize: 13,
|
child: Container(
|
||||||
),
|
color: Colors.grey[200],
|
||||||
),
|
child: const Center(
|
||||||
const SizedBox(height: 4),
|
child: Icon(Icons.fitness_center, size: 50),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
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),
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue