Traingsplan anlegen added

main
joschy2002 2025-05-30 20:54:25 +02:00
parent d706f8e9a2
commit 9689d4b9ba
2 changed files with 448 additions and 171 deletions

View File

@ -126,13 +126,14 @@ class _CalendarTabState extends State<CalendarTab> {
final cancelledTrainings = trainerData['cancelledTrainings'] as List<dynamic>? ?? [];
final trainingExercises = trainerData['trainingExercises'] as Map<String, dynamic>? ?? {};
// 1. Wochentagsbasierte Trainings (wie gehabt)
trainingTimes.forEach((day, timeStr) {
// Prüfe, ob der Key KEIN Datum ist (also ein Wochentag)
if (DateTime.tryParse(day) != null) return;
if (timeStr == null) return;
final duration = trainingDurations[day] as int? ?? 60;
final timeParts = (timeStr as String).split(':');
if (timeParts.length != 2) return;
final hour = int.tryParse(timeParts[0]) ?? 0;
final minute = int.tryParse(timeParts[1]) ?? 0;
@ -144,7 +145,6 @@ class _CalendarTabState extends State<CalendarTab> {
final date = eventDate.add(Duration(days: i * 7));
final normalizedDate = DateTime(date.year, date.month, date.day);
final dateString = normalizedDate.toIso8601String();
final isCancelled = cancelledTrainings.any((cancelled) {
if (cancelled is Map<String, dynamic>) {
final cancelledDate = DateTime.parse(cancelled['date'] as String);
@ -152,8 +152,11 @@ class _CalendarTabState extends State<CalendarTab> {
}
return false;
});
if (!isCancelled) {
// NEU: Prüfe, ob es ein datumsspezifisches Training für diesen Tag gibt
final hasDateSpecific = trainingTimes.containsKey(dateString);
if (!isCancelled || hasDateSpecific) {
// Wenn es ein datumsspezifisches Training gibt, blende das wochentagsbasierte Training aus
if (isCancelled && hasDateSpecific) continue;
final exercises = trainingExercises[dateString] as List<dynamic>? ?? [];
final totalExerciseDuration = exercises.fold<int>(0, (sum, exercise) {
if (exercise is Map<String, dynamic>) {
@ -161,7 +164,6 @@ class _CalendarTabState extends State<CalendarTab> {
}
return sum;
});
final event = {
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer',
'time': timeStr,
@ -174,7 +176,6 @@ class _CalendarTabState extends State<CalendarTab> {
'remainingTime': duration - totalExerciseDuration,
'club': trainerData['club'] ?? 'Kein Verein',
};
if (events.containsKey(normalizedDate)) {
events[normalizedDate]!.add(event);
} else {
@ -183,6 +184,47 @@ class _CalendarTabState extends State<CalendarTab> {
}
}
});
// 2. Datumsspezifische Trainings (NEU)
trainingTimes.forEach((key, timeStr) {
// Prüfe, ob der Key ein Datum ist
final date = DateTime.tryParse(key);
if (date == null) return;
if (timeStr == null) return;
final duration = trainingDurations[key] as int? ?? 60;
final timeParts = (timeStr as String).split(':');
if (timeParts.length != 2) return;
final hour = int.tryParse(timeParts[0]) ?? 0;
final minute = int.tryParse(timeParts[1]) ?? 0;
final normalizedDate = DateTime(date.year, date.month, date.day);
final dateString = normalizedDate.toIso8601String();
// Für datumsspezifische Trainings: cancelledTrainings ignorieren!
// Event immer erzeugen
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,
'duration': duration,
'trainerId': trainerDoc.id,
'isCurrentUser': trainerDoc.id == _currentUserId,
'day': key,
'date': dateString,
'exercises': exercises,
'remainingTime': duration - totalExerciseDuration,
'club': trainerData['club'] ?? 'Kein Verein',
};
if (events.containsKey(normalizedDate)) {
events[normalizedDate]!.add(event);
} else {
events[normalizedDate] = [event];
}
});
}
setState(() {
@ -269,9 +311,21 @@ class _CalendarTabState extends State<CalendarTab> {
final data = userDoc.data() as Map<String, dynamic>;
final cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []);
final trainingTimes = Map<String, dynamic>.from(data['trainingTimes'] ?? {});
final trainingDurations = Map<String, dynamic>.from(data['trainingDurations'] ?? {});
final trainingExercises = Map<String, dynamic>.from(data['trainingExercises'] ?? {});
// Stelle sicher, dass das Datum im richtigen Format gespeichert wird
final date = DateTime.parse(event['date']);
final normalizedDateString = DateTime(date.year, date.month, date.day).toIso8601String();
// Entferne ALLE Einträge für das Datum
trainingTimes.remove(normalizedDateString);
trainingDurations.remove(normalizedDateString);
trainingExercises.remove(normalizedDateString);
cancelledTrainings.add({
'date': event['date'],
'date': normalizedDateString,
'day': event['day'],
'time': event['time'],
'duration': event['duration'],
@ -282,6 +336,9 @@ class _CalendarTabState extends State<CalendarTab> {
.doc(_currentUserId)
.update({
'cancelledTrainings': cancelledTrainings,
'trainingTimes': trainingTimes,
'trainingDurations': trainingDurations,
'trainingExercises': trainingExercises,
});
await _loadEvents();
@ -371,6 +428,7 @@ class _CalendarTabState extends State<CalendarTab> {
@override
Widget build(BuildContext context) {
final isTrainer = _userRole == 'trainer';
return Scaffold(
appBar: AppBar(
title: const Text('Kalender'),
@ -449,203 +507,368 @@ class _CalendarTabState extends State<CalendarTab> {
},
),
),
if (isTrainer && _selectedDay != null) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('Training an diesem Tag hinzufügen'),
onPressed: () async {
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => _TrainingEditDialog(
date: _selectedDay!,
),
);
if (result != null) {
await _addOrEditTraining(_selectedDay!, result['time'], result['duration'], isException: false);
await _loadEvents();
}
},
),
),
),
],
const Divider(),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _selectedDay == null
? const Center(child: Text('Bitte wähle einen Tag aus'))
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _getEventsForDay(_selectedDay!).length,
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;
: (() {
final events = _getEventsForDay(_selectedDay!);
// Sortiere nach Uhrzeit (Format: 'HH:mm')
events.sort((a, b) {
final aTime = a['time'] as String? ?? '00:00';
final bTime = b['time'] as String? ?? '00:00';
final aParts = aTime.split(':').map(int.parse).toList();
final bParts = bTime.split(':').map(int.parse).toList();
final aMinutes = aParts[0] * 60 + aParts[1];
final bMinutes = bParts[0] * 60 + bParts[1];
return aMinutes.compareTo(bMinutes);
});
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: events.length,
itemBuilder: (context, index) {
final event = events[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: Column(
children: [
ListTile(
leading: Icon(
Icons.sports,
color: categoryColors[event['day']] ?? (isCurrentUser ? Colors.blue : Colors.grey),
),
title: Text(
isCurrentUser ? 'Training' : event['trainerName'],
style: TextStyle(
fontWeight: isCurrentUser ? FontWeight.bold : null,
return Card(
margin: const EdgeInsets.only(bottom: 8),
color: isCurrentUser ? Colors.blue.withOpacity(0.1) : null,
child: Column(
children: [
ListTile(
leading: Icon(
Icons.sports,
color: categoryColors[event['day']] ?? (isCurrentUser ? Colors.blue : Colors.grey),
),
),
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'),
),
],
),
);
},
title: Text(
isCurrentUser ? 'Training' : event['trainerName'],
style: TextStyle(
fontWeight: isCurrentUser ? FontWeight.bold : null,
),
if (isCurrentUser)
),
subtitle: Text(
'${event['time']} - ${event['duration']} Minuten\nVerbleibende Zeit: $remainingTime Minuten',
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isCurrentUser)
IconButton(
icon: const Icon(Icons.edit),
onPressed: () async {
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => _TrainingEditDialog(
date: _selectedDay!,
initialTime: event['time'],
initialDuration: event['duration'],
),
);
if (result != null) {
await _addOrEditTraining(_selectedDay!, result['time'], result['duration'], isException: true);
await _loadEvents();
}
},
),
if (isCurrentUser)
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _addExercise(event),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
icon: const Icon(Icons.info),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Training löschen'),
content: const Text('Möchten Sie dieses Training wirklich löschen?'),
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('Abbrechen'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_deleteTraining(event);
},
child: const Text('Löschen', style: TextStyle(color: Colors.red)),
child: const Text('Schließen'),
),
],
),
);
},
),
],
),
),
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),
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'),
),
),
if (isCurrentUser)
IconButton(
icon: const Icon(Icons.delete, color: Colors.red, size: 20),
TextButton(
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)),
),
],
),
);
Navigator.pop(context);
_deleteTraining(event);
},
child: const Text('Löschen', style: TextStyle(color: Colors.red)),
),
],
),
);
}
return const SizedBox.shrink();
}),
],
),
);
},
),
],
),
),
],
),
);
},
),
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,
),
),
),
),
],
),
);
}
return const SizedBox.shrink();
}),
],
),
),
],
),
);
},
);
})()
),
],
),
),
);
}
Future<void> _addOrEditTraining(DateTime date, String time, int duration, {bool isException = false}) async {
// Trainingsdatum als String
final dateString = DateTime(date.year, date.month, date.day).toIso8601String();
final userDoc = await FirebaseFirestore.instance.collection('User').doc(_currentUserId).get();
final data = userDoc.data() ?? {};
final trainingExercises = Map<String, dynamic>.from(data['trainingExercises'] ?? {});
final trainingTimes = Map<String, dynamic>.from(data['trainingTimes'] ?? {});
final trainingDurations = Map<String, dynamic>.from(data['trainingDurations'] ?? {});
// Entferne nur das Training für das konkrete Datum
trainingExercises.remove(dateString);
trainingTimes.remove(dateString);
trainingDurations.remove(dateString);
// Neues Training speichern (nur mit Datum als Key)
trainingExercises[dateString] = [];
trainingTimes[dateString] = time;
trainingDurations[dateString] = duration;
// cancelledTrainings bereinigen und ggf. Eintrag hinzufügen, wenn an diesem Tag ein wochentagsbasiertes Training existiert
final cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []);
cancelledTrainings.removeWhere((cancelled) =>
cancelled is Map<String, dynamic> &&
cancelled.containsKey('date') &&
cancelled['date'] == dateString
);
// Prüfe, ob für diesen Tag ein wochentagsbasiertes Training existiert und ob es eine Ausnahme ist
final weekdayNames = [
'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'
];
final weekdayKey = weekdayNames[date.weekday - 1];
if (isException && trainingTimes.containsKey(weekdayKey)) {
cancelledTrainings.add({
'date': dateString,
'day': weekdayKey,
'time': trainingTimes[weekdayKey],
'duration': trainingDurations[weekdayKey] ?? 60,
});
}
await FirebaseFirestore.instance.collection('User').doc(_currentUserId).update({
'trainingExercises': trainingExercises,
'trainingTimes': trainingTimes,
'trainingDurations': trainingDurations,
'cancelledTrainings': cancelledTrainings,
});
}
}
class _TrainingEditDialog extends StatefulWidget {
final DateTime date;
final String? initialTime;
final int? initialDuration;
const _TrainingEditDialog({required this.date, this.initialTime, this.initialDuration});
@override
State<_TrainingEditDialog> createState() => _TrainingEditDialogState();
}
class _TrainingEditDialogState extends State<_TrainingEditDialog> {
TimeOfDay? _selectedTime;
int _duration = 60;
@override
void initState() {
super.initState();
if (widget.initialTime != null) {
final parts = widget.initialTime!.split(':');
if (parts.length == 2) {
_selectedTime = TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
}
}
if (widget.initialDuration != null) {
_duration = widget.initialDuration!;
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Training bearbeiten (${widget.date.day}.${widget.date.month}.${widget.date.year})'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.access_time),
title: const Text('Uhrzeit wählen'),
subtitle: Text(_selectedTime != null ? _selectedTime!.format(context) : 'Keine Uhrzeit gewählt'),
onTap: () async {
final picked = await showTimePicker(
context: context,
initialTime: _selectedTime ?? TimeOfDay.now(),
);
if (picked != null) {
setState(() => _selectedTime = picked);
}
},
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () {
if (_duration > 15) setState(() => _duration -= 15);
},
),
Text('$_duration Minuten', style: const TextStyle(fontSize: 18)),
IconButton(
icon: const Icon(Icons.add),
onPressed: () => setState(() => _duration += 15),
),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Abbrechen'),
),
ElevatedButton(
onPressed: _selectedTime != null
? () {
final timeString = _selectedTime!.hour.toString().padLeft(2, '0') + ':' + _selectedTime!.minute.toString().padLeft(2, '0');
Navigator.pop(context, {'time': timeString, 'duration': _duration});
}
: null,
child: const Text('Speichern'),
),
],
);
}
}

View File

@ -89,11 +89,65 @@ class _ProfileTabState extends State<ProfileTab> {
final timeString = '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
// Update training times and durations
await FirebaseFirestore.instance.collection('User').doc(user.uid).update({
'trainingTimes.$day': timeString,
'trainingDurations.$day': duration,
});
// Trainings ab 1. Januar des aktuellen Jahres bis 52 Wochen in die Zukunft anlegen
final now = DateTime.now();
final yearStart = DateTime(now.year, 1, 1);
final weekdays = {
'Montag': 1,
'Dienstag': 2,
'Mittwoch': 3,
'Donnerstag': 4,
'Freitag': 5,
'Samstag': 6,
'Sonntag': 7,
};
final targetWeekday = weekdays[day] ?? 1;
// Finde das erste gewünschte Wochentags-Datum ab Jahresanfang
DateTime firstDate = yearStart;
while (firstDate.weekday != targetWeekday) {
firstDate = firstDate.add(const Duration(days: 1));
}
final newTrainingDates = <String>[];
for (var i = 0; i < 52; i++) {
final date = firstDate.add(Duration(days: i * 7));
final normalizedDate = DateTime(date.year, date.month, date.day, time.hour, time.minute);
final dateString = DateTime(normalizedDate.year, normalizedDate.month, normalizedDate.day).toIso8601String();
newTrainingDates.add(dateString);
}
// Load existing trainingExercises and cancelledTrainings
final userDoc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get();
final data = userDoc.data() ?? {};
final trainingExercises = Map<String, dynamic>.from(data['trainingExercises'] ?? {});
final cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []);
// Add empty training only if not already present
for (final dateString in newTrainingDates) {
if (!trainingExercises.containsKey(dateString)) {
trainingExercises[dateString] = [];
}
}
// Remove cancelledTrainings for these dates
cancelledTrainings.removeWhere((cancelled) =>
cancelled is Map<String, dynamic> &&
cancelled.containsKey('date') &&
newTrainingDates.contains(cancelled['date'])
);
await FirebaseFirestore.instance.collection('User').doc(user.uid).update({
'cancelledTrainings': cancelledTrainings,
'trainingExercises': trainingExercises,
});
setState(() {
_trainingTimes[day] = time;
_trainingDurations[day] = duration;