1280 lines
55 KiB
Dart
1280 lines
55 KiB
Dart
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';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
const Map<String, Color> categoryColors = {
|
|
'Aufwärmen & Mobilisation': Colors.deepOrange,
|
|
'Wurf- & Torabschluss': Colors.orange,
|
|
'Torwarttraining': Colors.green,
|
|
'Athletik': Colors.blue,
|
|
'Pass': Colors.purple,
|
|
'Koordination': Colors.teal,
|
|
};
|
|
|
|
class CalendarTab extends StatefulWidget {
|
|
final DateTime? initialDate;
|
|
const CalendarTab({super.key, this.initialDate});
|
|
|
|
@override
|
|
State<CalendarTab> createState() => _CalendarTabState();
|
|
}
|
|
|
|
class _CalendarTabState extends State<CalendarTab> {
|
|
CalendarFormat _calendarFormat = CalendarFormat.week;
|
|
late DateTime _focusedDay;
|
|
DateTime? _selectedDay;
|
|
Map<DateTime, List<Map<String, dynamic>>> _events = {};
|
|
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() {
|
|
super.initState();
|
|
_focusedDay = widget.initialDate ?? DateTime.now();
|
|
_selectedDay = _focusedDay;
|
|
_currentUserId = FirebaseAuth.instance.currentUser?.uid;
|
|
_initializeData();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant CalendarTab oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (widget.initialDate != null && widget.initialDate != oldWidget.initialDate) {
|
|
setState(() {
|
|
_focusedDay = widget.initialDate!;
|
|
_selectedDay = widget.initialDate!;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _initializeData() async {
|
|
if (_currentUserId != null) {
|
|
final userDoc = await FirebaseFirestore.instance
|
|
.collection('User')
|
|
.doc(_currentUserId)
|
|
.get();
|
|
|
|
if (userDoc.exists) {
|
|
setState(() {
|
|
_userRole = userDoc.data()?['role'] as String?;
|
|
});
|
|
await _loadEvents();
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _loadEvents() async {
|
|
if (_userRole == null) return;
|
|
|
|
setState(() => _isLoading = true);
|
|
try {
|
|
QuerySnapshot trainersSnapshot;
|
|
|
|
if (_userRole == 'trainer') {
|
|
trainersSnapshot = await FirebaseFirestore.instance
|
|
.collection('User')
|
|
.where('role', isEqualTo: 'trainer')
|
|
.where(FieldPath.documentId, isEqualTo: _currentUserId)
|
|
.get();
|
|
} else {
|
|
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();
|
|
}
|
|
|
|
final events = <DateTime, List<Map<String, dynamic>>>{};
|
|
|
|
for (var trainerDoc in trainersSnapshot.docs) {
|
|
final trainerData = trainerDoc.data() as Map<String, dynamic>;
|
|
final trainings = trainerData['trainings'] as Map<String, dynamic>? ?? {};
|
|
final cancelledTrainings = trainerData['cancelledTrainings'] as List<dynamic>? ?? [];
|
|
final trainingTimes = trainerData['trainingTimes'] as Map<String, dynamic>? ?? {};
|
|
final trainingDurations = trainerData['trainingDurations'] as Map<String, dynamic>? ?? {};
|
|
|
|
print('Lade Events:');
|
|
print('trainings: $trainings');
|
|
print('cancelledTrainings: $cancelledTrainings');
|
|
print('trainingTimes: $trainingTimes');
|
|
print('trainingDurations: $trainingDurations');
|
|
|
|
// Lade alle Trainings für das Datum
|
|
trainings.forEach((dateString, trainingsList) {
|
|
final date = DateTime.tryParse(dateString);
|
|
if (date == null) return;
|
|
final normalizedDate = DateTime(date.year, date.month, date.day);
|
|
final list = List<Map<String, dynamic>>.from(trainingsList);
|
|
for (final training in list) {
|
|
final timeStr = training['time'] as String? ?? '00:00';
|
|
final duration = training['duration'] as int? ?? 60;
|
|
final exercises = training['exercises'] 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': dateString,
|
|
'date': dateString,
|
|
'exercises': exercises,
|
|
'remainingTime': duration - totalExerciseDuration,
|
|
'club': trainerData['club'] ?? 'Kein Verein',
|
|
'id': training['id'],
|
|
};
|
|
if (events.containsKey(normalizedDate)) {
|
|
events[normalizedDate]!.add(event);
|
|
} else {
|
|
events[normalizedDate] = [event];
|
|
}
|
|
}
|
|
});
|
|
|
|
// Füge regelmäßige Trainings hinzu
|
|
final now = DateTime.now();
|
|
final yearStart = DateTime(now.year, 1, 1);
|
|
final yearEnd = DateTime(now.year + 1, 1, 1);
|
|
final weekdays = {
|
|
'Montag': 1,
|
|
'Dienstag': 2,
|
|
'Mittwoch': 3,
|
|
'Donnerstag': 4,
|
|
'Freitag': 5,
|
|
'Samstag': 6,
|
|
'Sonntag': 7,
|
|
};
|
|
|
|
// Erstelle eine Set von gecancelten Daten für schnelleren Zugriff
|
|
final cancelledDates = cancelledTrainings
|
|
.where((cancelled) => cancelled is Map<String, dynamic>)
|
|
.map((cancelled) => DateTime.parse((cancelled as Map<String, dynamic>)['date'] as String))
|
|
.toSet();
|
|
|
|
print('Gecancelte Daten: $cancelledDates');
|
|
|
|
trainingTimes.forEach((day, timeStr) {
|
|
if (timeStr == null) return;
|
|
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 duration = trainingDurations[day] ?? 60;
|
|
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));
|
|
}
|
|
|
|
// Generiere Trainings für das gesamte Jahr
|
|
while (firstDate.isBefore(yearEnd)) {
|
|
final normalizedDate = DateTime(firstDate.year, firstDate.month, firstDate.day);
|
|
final dateString = normalizedDate.toIso8601String();
|
|
|
|
// Prüfe, ob das Training für dieses Datum gecancelt ist
|
|
final isCancelled = cancelledDates.any((cancelledDate) =>
|
|
isSameDay(cancelledDate, normalizedDate)
|
|
);
|
|
|
|
// Prüfe, ob bereits ein spezifisches Training für dieses Datum existiert
|
|
final hasSpecificTraining = trainings.containsKey(dateString);
|
|
|
|
if (!isCancelled && !hasSpecificTraining) {
|
|
final event = {
|
|
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer',
|
|
'time': timeStr,
|
|
'duration': duration,
|
|
'trainerId': trainerDoc.id,
|
|
'isCurrentUser': trainerDoc.id == _currentUserId,
|
|
'day': day,
|
|
'date': dateString,
|
|
'exercises': [],
|
|
'remainingTime': duration,
|
|
'club': trainerData['club'] ?? 'Kein Verein',
|
|
'id': 'weekly_${day}_${dateString}',
|
|
};
|
|
if (events.containsKey(normalizedDate)) {
|
|
events[normalizedDate]!.add(event);
|
|
} else {
|
|
events[normalizedDate] = [event];
|
|
}
|
|
}
|
|
firstDate = firstDate.add(const Duration(days: 7));
|
|
}
|
|
});
|
|
}
|
|
|
|
setState(() {
|
|
_events = events;
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
print('Error loading events: $e');
|
|
setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
|
|
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 {
|
|
print('Übung ausgewählt:');
|
|
print('ID: ${result['id']}');
|
|
print('Titel: ${result['title']}');
|
|
print('Vollständige Übungsdaten: $result');
|
|
|
|
bool shouldAddExercise = true;
|
|
|
|
// Prüfe die Bewertungen der Übung in der Training-Collection
|
|
print('Suche Übung in der Datenbank...');
|
|
final exerciseDoc = await FirebaseFirestore.instance
|
|
.collection('Training')
|
|
.doc(result['id'])
|
|
.get();
|
|
|
|
print('Übungsdokument gefunden: ${exerciseDoc.exists}');
|
|
print('Collection-Pfad: Training/${result['id']}');
|
|
|
|
if (exerciseDoc.exists) {
|
|
final exerciseData = exerciseDoc.data() as Map<String, dynamic>;
|
|
print('Übungsdaten: $exerciseData');
|
|
|
|
// Versuche die Bewertungen aus verschiedenen möglichen Feldern zu lesen
|
|
List<Map<String, dynamic>> ratings = [];
|
|
if (exerciseData.containsKey('ratings')) {
|
|
ratings = List<Map<String, dynamic>>.from(exerciseData['ratings'] ?? []);
|
|
print('Bewertungen aus Feld "ratings" gefunden: $ratings');
|
|
} else if (exerciseData.containsKey('rating')) {
|
|
ratings = List<Map<String, dynamic>>.from(exerciseData['rating'] ?? []);
|
|
print('Bewertungen aus Feld "rating" gefunden: $ratings');
|
|
}
|
|
|
|
print('Gefundene Bewertungen: $ratings');
|
|
|
|
if (ratings.isNotEmpty) {
|
|
// Berechne den Durchschnitt der Bewertungen
|
|
double totalRating = 0;
|
|
for (var rating in ratings) {
|
|
if (rating.containsKey('rating')) {
|
|
totalRating += (rating['rating'] as num).toDouble();
|
|
}
|
|
}
|
|
final averageRating = totalRating / ratings.length;
|
|
|
|
print('Durchschnittliche Bewertung: $averageRating');
|
|
|
|
// Wenn die durchschnittliche Bewertung unter 3 liegt, zeige eine Warnung
|
|
if (averageRating < 3) {
|
|
print('Zeige Warnung für schlechte Bewertung');
|
|
if (mounted) {
|
|
shouldAddExercise = await showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Hinweis zur Übungsbewertung'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Die Übung "${result['title']}" wurde von anderen Trainern mit ${averageRating.toStringAsFixed(1)} von 5 Sternen bewertet.'),
|
|
const SizedBox(height: 16),
|
|
const Text('Möchten Sie diese Übung trotzdem zu Ihrem Training hinzufügen?'),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Abbrechen'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: const Text('Trotzdem hinzufügen'),
|
|
),
|
|
],
|
|
),
|
|
) ?? false;
|
|
|
|
print('Benutzer hat entschieden: ${shouldAddExercise ? "Hinzufügen" : "Abbrechen"}');
|
|
}
|
|
} else {
|
|
print('Bewertung ist gut genug, keine Warnung nötig');
|
|
}
|
|
} else {
|
|
print('Keine Bewertungen vorhanden');
|
|
}
|
|
} else {
|
|
print('Warnung: Übungsdokument nicht gefunden');
|
|
}
|
|
|
|
if (!shouldAddExercise) {
|
|
print('Übung wird nicht hinzugefügt (Benutzer hat abgebrochen)');
|
|
return;
|
|
}
|
|
|
|
// Füge die Übung zum Training hinzu
|
|
final userDoc = await FirebaseFirestore.instance
|
|
.collection('User')
|
|
.doc(_currentUserId)
|
|
.get();
|
|
|
|
if (!userDoc.exists) return;
|
|
|
|
final data = userDoc.data() as Map<String, dynamic>;
|
|
final trainings = Map<String, dynamic>.from(data['trainings'] ?? {});
|
|
final dateString = event['date'] as String;
|
|
final trainingId = event['id'] as String;
|
|
|
|
print('Füge Übung zum Training hinzu:');
|
|
print('dateString: $dateString');
|
|
print('trainingId: $trainingId');
|
|
print('Vorher - trainings: $trainings');
|
|
|
|
// Finde das Training und füge die Übung hinzu
|
|
if (trainings.containsKey(dateString)) {
|
|
final trainingsList = List<Map<String, dynamic>>.from(trainings[dateString]);
|
|
final trainingIndex = trainingsList.indexWhere((t) => t['id'] == trainingId);
|
|
|
|
if (trainingIndex != -1) {
|
|
final exercises = List<Map<String, dynamic>>.from(trainingsList[trainingIndex]['exercises'] ?? []);
|
|
exercises.add({
|
|
'id': result['id'],
|
|
'name': result['title'],
|
|
'description': result['description'],
|
|
'duration': result['duration'],
|
|
});
|
|
trainingsList[trainingIndex]['exercises'] = exercises;
|
|
trainings[dateString] = trainingsList;
|
|
}
|
|
}
|
|
|
|
print('Nachher - trainings: $trainings');
|
|
|
|
// Aktualisiere die Datenbank
|
|
await FirebaseFirestore.instance
|
|
.collection('User')
|
|
.doc(_currentUserId)
|
|
.update({
|
|
'trainings': trainings,
|
|
});
|
|
|
|
// Aktualisiere die UI sofort
|
|
setState(() {
|
|
final normalizedDate = DateTime.parse(dateString);
|
|
if (_events.containsKey(normalizedDate)) {
|
|
final eventIndex = _events[normalizedDate]!.indexWhere((e) => e['id'] == trainingId);
|
|
if (eventIndex != -1) {
|
|
final exercises = List<Map<String, dynamic>>.from(_events[normalizedDate]![eventIndex]['exercises'] ?? []);
|
|
exercises.add({
|
|
'id': result['id'],
|
|
'name': result['title'],
|
|
'description': result['description'],
|
|
'duration': result['duration'],
|
|
});
|
|
_events[normalizedDate]![eventIndex]['exercises'] = exercises;
|
|
_events[normalizedDate]![eventIndex]['remainingTime'] =
|
|
(_events[normalizedDate]![eventIndex]['duration'] as int) -
|
|
exercises.fold<int>(0, (sum, exercise) => sum + (exercise['duration'] as int));
|
|
}
|
|
}
|
|
});
|
|
|
|
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;
|
|
try {
|
|
final userDoc = await FirebaseFirestore.instance.collection('User').doc(_currentUserId).get();
|
|
if (!userDoc.exists) return;
|
|
final data = userDoc.data() as Map<String, dynamic>;
|
|
final trainings = Map<String, dynamic>.from(data['trainings'] ?? {});
|
|
final dateString = event['date'] as String;
|
|
final trainingId = event['id'] as String?;
|
|
final cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []);
|
|
|
|
print('Lösche Training:');
|
|
print('dateString: $dateString');
|
|
print('trainingId: $trainingId');
|
|
print('Vorher - trainings: $trainings');
|
|
print('Vorher - cancelledTrainings: $cancelledTrainings');
|
|
|
|
// Wenn es sich um ein regelmäßiges Training handelt (ID beginnt mit 'weekly_')
|
|
if (trainingId != null && trainingId.startsWith('weekly_')) {
|
|
// Füge das Datum zu cancelledTrainings hinzu, wenn es noch nicht existiert
|
|
if (!cancelledTrainings.any((cancelled) =>
|
|
cancelled is Map<String, dynamic> &&
|
|
cancelled['date'] == dateString
|
|
)) {
|
|
cancelledTrainings.add({
|
|
'date': dateString,
|
|
});
|
|
}
|
|
} else {
|
|
// Für spezifische Trainings: Entferne das Training aus der Liste
|
|
if (trainings.containsKey(dateString)) {
|
|
final trainingsList = List<Map<String, dynamic>>.from(trainings[dateString]);
|
|
trainingsList.removeWhere((t) => t['id'] == trainingId);
|
|
|
|
if (trainingsList.isEmpty) {
|
|
trainings.remove(dateString);
|
|
} else {
|
|
trainings[dateString] = trainingsList;
|
|
}
|
|
}
|
|
|
|
// Entferne den Eintrag aus cancelledTrainings nur, wenn es sich um ein spezifisches Training handelt
|
|
// und kein regelmäßiges Training an diesem Tag stattfindet
|
|
final weekdays = {
|
|
'Montag': 1,
|
|
'Dienstag': 2,
|
|
'Mittwoch': 3,
|
|
'Donnerstag': 4,
|
|
'Freitag': 5,
|
|
'Samstag': 6,
|
|
'Sonntag': 7,
|
|
};
|
|
|
|
final date = DateTime.parse(dateString);
|
|
final weekday = date.weekday;
|
|
final weekdayName = weekdays.entries.firstWhere((entry) => entry.value == weekday).key;
|
|
final trainingTimes = data['trainingTimes'] as Map<String, dynamic>? ?? {};
|
|
|
|
// Wenn an diesem Tag kein regelmäßiges Training stattfindet, entferne den Eintrag aus cancelledTrainings
|
|
if (!trainingTimes.containsKey(weekdayName)) {
|
|
cancelledTrainings.removeWhere((cancelled) =>
|
|
cancelled is Map<String, dynamic> &&
|
|
cancelled.containsKey('date') &&
|
|
cancelled['date'] == dateString
|
|
);
|
|
}
|
|
}
|
|
|
|
print('Nachher - trainings: $trainings');
|
|
print('Nachher - cancelledTrainings: $cancelledTrainings');
|
|
|
|
// Aktualisiere die Datenbank
|
|
final updates = <String, dynamic>{};
|
|
|
|
// Aktualisiere trainings nur, wenn es nicht leer ist
|
|
if (trainings.isNotEmpty) {
|
|
updates['trainings'] = trainings;
|
|
} else {
|
|
updates['trainings'] = null;
|
|
}
|
|
|
|
// Aktualisiere cancelledTrainings nur, wenn es nicht leer ist
|
|
if (cancelledTrainings.isNotEmpty) {
|
|
updates['cancelledTrainings'] = cancelledTrainings;
|
|
} else {
|
|
updates['cancelledTrainings'] = null;
|
|
}
|
|
|
|
// Führe die Aktualisierung durch
|
|
await FirebaseFirestore.instance.collection('User').doc(_currentUserId).update(updates);
|
|
|
|
// Aktualisiere die UI sofort
|
|
setState(() {
|
|
final normalizedDate = DateTime.parse(dateString);
|
|
if (_events.containsKey(normalizedDate)) {
|
|
_events[normalizedDate]!.removeWhere((e) => e['id'] == trainingId);
|
|
if (_events[normalizedDate]!.isEmpty) {
|
|
_events.remove(normalizedDate);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Training wurde gelöscht')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
print('Error deleting training: $e');
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Fehler beim Löschen des Trainings')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 trainings = Map<String, dynamic>.from(data['trainings'] ?? {});
|
|
final dateString = event['date'] as String;
|
|
final trainingId = event['id'] as String;
|
|
|
|
print('Entferne Übung:');
|
|
print('dateString: $dateString');
|
|
print('trainingId: $trainingId');
|
|
print('Vorher - trainings: $trainings');
|
|
|
|
// Finde das Training und entferne die Übung
|
|
if (trainings.containsKey(dateString)) {
|
|
final trainingsList = List<Map<String, dynamic>>.from(trainings[dateString]);
|
|
final trainingIndex = trainingsList.indexWhere((t) => t['id'] == trainingId);
|
|
|
|
if (trainingIndex != -1) {
|
|
final exercises = List<Map<String, dynamic>>.from(trainingsList[trainingIndex]['exercises'] ?? []);
|
|
exercises.removeWhere((e) => e['id'] == exercise['id']);
|
|
trainingsList[trainingIndex]['exercises'] = exercises;
|
|
trainings[dateString] = trainingsList;
|
|
}
|
|
}
|
|
|
|
print('Nachher - trainings: $trainings');
|
|
|
|
// Aktualisiere die Datenbank
|
|
await FirebaseFirestore.instance
|
|
.collection('User')
|
|
.doc(_currentUserId)
|
|
.update({
|
|
'trainings': trainings,
|
|
});
|
|
|
|
// Aktualisiere die UI sofort
|
|
setState(() {
|
|
final normalizedDate = DateTime.parse(dateString);
|
|
if (_events.containsKey(normalizedDate)) {
|
|
final eventIndex = _events[normalizedDate]!.indexWhere((e) => e['id'] == trainingId);
|
|
if (eventIndex != -1) {
|
|
final exercises = List<Map<String, dynamic>>.from(_events[normalizedDate]![eventIndex]['exercises'] ?? []);
|
|
exercises.removeWhere((e) => e['id'] == exercise['id']);
|
|
_events[normalizedDate]![eventIndex]['exercises'] = exercises;
|
|
_events[normalizedDate]![eventIndex]['remainingTime'] =
|
|
(_events[normalizedDate]![eventIndex]['duration'] as int) -
|
|
exercises.fold<int>(0, (sum, exercise) => sum + (exercise['duration'] as int));
|
|
}
|
|
}
|
|
});
|
|
|
|
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,
|
|
'Dienstag': 2,
|
|
'Mittwoch': 3,
|
|
'Donnerstag': 4,
|
|
'Freitag': 5,
|
|
'Samstag': 6,
|
|
'Sonntag': 7,
|
|
};
|
|
|
|
final targetWeekday = weekdays[day] ?? 1;
|
|
var daysUntilNext = targetWeekday - currentWeekday;
|
|
if (daysUntilNext <= 0) {
|
|
daysUntilNext += 7;
|
|
}
|
|
return daysUntilNext;
|
|
}
|
|
|
|
List<Map<String, dynamic>> _getEventsForDay(DateTime day) {
|
|
final normalizedDate = DateTime(day.year, day.month, day.day);
|
|
return _events[normalizedDate] ?? [];
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isTrainer = _userRole == 'trainer';
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Kalender'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: _isLoading ? null : _loadEvents,
|
|
),
|
|
],
|
|
),
|
|
body: SafeArea(
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: TableCalendar(
|
|
firstDay: DateTime.utc(2024, 1, 1),
|
|
lastDay: DateTime.utc(2025, 12, 31),
|
|
focusedDay: _focusedDay,
|
|
calendarFormat: _calendarFormat,
|
|
locale: 'de_DE',
|
|
selectedDayPredicate: (day) {
|
|
return isSameDay(_selectedDay, day);
|
|
},
|
|
onDaySelected: (selectedDay, focusedDay) {
|
|
setState(() {
|
|
_selectedDay = selectedDay;
|
|
_focusedDay = focusedDay;
|
|
});
|
|
},
|
|
onFormatChanged: (format) {
|
|
setState(() {
|
|
_calendarFormat = format;
|
|
});
|
|
},
|
|
onPageChanged: (focusedDay) {
|
|
setState(() {
|
|
_focusedDay = focusedDay;
|
|
});
|
|
},
|
|
eventLoader: _getEventsForDay,
|
|
calendarStyle: const CalendarStyle(
|
|
markersMaxCount: 1,
|
|
markerDecoration: BoxDecoration(
|
|
color: Colors.blue,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
headerStyle: const HeaderStyle(
|
|
formatButtonVisible: true,
|
|
titleCentered: true,
|
|
),
|
|
calendarBuilders: CalendarBuilders(
|
|
markerBuilder: (context, date, events) {
|
|
if (events.isEmpty) return null;
|
|
return Positioned(
|
|
bottom: 1,
|
|
child: Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.blue,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
startingDayOfWeek: StartingDayOfWeek.monday,
|
|
daysOfWeekStyle: const DaysOfWeekStyle(
|
|
weekdayStyle: TextStyle(fontWeight: FontWeight.bold),
|
|
weekendStyle: TextStyle(color: Colors.red),
|
|
),
|
|
availableCalendarFormats: const {
|
|
CalendarFormat.week: 'Woche',
|
|
CalendarFormat.month: 'Monat',
|
|
},
|
|
),
|
|
),
|
|
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'))
|
|
: (() {
|
|
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,
|
|
),
|
|
),
|
|
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'],
|
|
trainingId: event['id'],
|
|
),
|
|
);
|
|
if (result != null) {
|
|
await _addOrEditTraining(
|
|
_selectedDay!,
|
|
result['time'],
|
|
result['duration'],
|
|
isException: true,
|
|
trainingId: result['trainingId'],
|
|
);
|
|
await _loadEvents();
|
|
}
|
|
},
|
|
),
|
|
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: 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: Text('Möchten Sie die Übung "${exercise['name']}" 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();
|
|
}),
|
|
],
|
|
],
|
|
),
|
|
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: Text('Möchten Sie die Übung "${exercise['name']}" 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();
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
})()
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _addOrEditTraining(DateTime date, String time, int duration, {bool isException = false, String? trainingId}) async {
|
|
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 trainings = Map<String, dynamic>.from(data['trainings'] ?? {});
|
|
final uuid = const Uuid();
|
|
final cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []);
|
|
final trainingTimes = data['trainingTimes'] as Map<String, dynamic>? ?? {};
|
|
|
|
print('Füge/Bearbeite Training:');
|
|
print('dateString: $dateString');
|
|
print('trainingId: $trainingId');
|
|
print('Vorher - trainings: $trainings');
|
|
print('Vorher - cancelledTrainings: $cancelledTrainings');
|
|
|
|
// Trainingsliste für das Datum holen oder neu anlegen
|
|
final trainingsList = List<Map<String, dynamic>>.from(trainings[dateString] ?? []);
|
|
|
|
if (trainingId != null) {
|
|
// Bearbeiten: Training mit ID ersetzen
|
|
final idx = trainingsList.indexWhere((t) => t['id'] == trainingId);
|
|
if (idx != -1) {
|
|
trainingsList[idx] = {
|
|
'id': trainingId,
|
|
'time': time,
|
|
'duration': duration,
|
|
'exercises': trainingsList[idx]['exercises'] ?? [],
|
|
};
|
|
}
|
|
} else {
|
|
// Hinzufügen: neues Training-Objekt anfügen
|
|
trainingsList.add({
|
|
'id': uuid.v4(),
|
|
'time': time,
|
|
'duration': duration,
|
|
'exercises': [],
|
|
});
|
|
}
|
|
|
|
trainings[dateString] = trainingsList;
|
|
|
|
// Prüfe, ob an diesem Tag ein regelmäßiges Training stattfindet
|
|
final weekdays = {
|
|
'Montag': 1,
|
|
'Dienstag': 2,
|
|
'Mittwoch': 3,
|
|
'Donnerstag': 4,
|
|
'Freitag': 5,
|
|
'Samstag': 6,
|
|
'Sonntag': 7,
|
|
};
|
|
|
|
final weekday = date.weekday;
|
|
final weekdayName = weekdays.entries.firstWhere((entry) => entry.value == weekday).key;
|
|
|
|
// Wenn an diesem Tag ein regelmäßiges Training stattfindet, füge es zu cancelledTrainings hinzu
|
|
if (trainingTimes.containsKey(weekdayName)) {
|
|
if (!cancelledTrainings.any((cancelled) =>
|
|
cancelled is Map<String, dynamic> &&
|
|
cancelled['date'] == dateString
|
|
)) {
|
|
cancelledTrainings.add({
|
|
'date': dateString,
|
|
});
|
|
}
|
|
} else {
|
|
// Wenn kein regelmäßiges Training stattfindet, entferne den Eintrag aus cancelledTrainings
|
|
cancelledTrainings.removeWhere((cancelled) =>
|
|
cancelled is Map<String, dynamic> &&
|
|
cancelled.containsKey('date') &&
|
|
cancelled['date'] == dateString
|
|
);
|
|
}
|
|
|
|
print('Nachher - trainings: $trainings');
|
|
print('Nachher - cancelledTrainings: $cancelledTrainings');
|
|
|
|
// Aktualisiere die Datenbank
|
|
final updates = <String, dynamic>{};
|
|
|
|
// Aktualisiere trainings nur, wenn es nicht leer ist
|
|
if (trainings.isNotEmpty) {
|
|
updates['trainings'] = trainings;
|
|
} else {
|
|
updates['trainings'] = null;
|
|
}
|
|
|
|
// Aktualisiere cancelledTrainings nur, wenn es nicht leer ist
|
|
if (cancelledTrainings.isNotEmpty) {
|
|
updates['cancelledTrainings'] = cancelledTrainings;
|
|
} else {
|
|
updates['cancelledTrainings'] = null;
|
|
}
|
|
|
|
// Führe die Aktualisierung durch
|
|
await FirebaseFirestore.instance.collection('User').doc(_currentUserId).update(updates);
|
|
|
|
// Aktualisiere die UI sofort
|
|
setState(() {
|
|
final normalizedDate = DateTime(date.year, date.month, date.day);
|
|
final event = {
|
|
'trainerName': data['name'] ?? 'Unbekannter Trainer',
|
|
'time': time,
|
|
'duration': duration,
|
|
'trainerId': _currentUserId,
|
|
'isCurrentUser': true,
|
|
'day': dateString,
|
|
'date': dateString,
|
|
'exercises': [],
|
|
'remainingTime': duration,
|
|
'club': data['club'] ?? 'Kein Verein',
|
|
'id': trainingId ?? uuid.v4(),
|
|
};
|
|
if (_events.containsKey(normalizedDate)) {
|
|
_events[normalizedDate]!.add(event);
|
|
} else {
|
|
_events[normalizedDate] = [event];
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
class _TrainingEditDialog extends StatefulWidget {
|
|
final DateTime date;
|
|
final String? initialTime;
|
|
final int? initialDuration;
|
|
final String? trainingId;
|
|
const _TrainingEditDialog({
|
|
required this.date,
|
|
this.initialTime,
|
|
this.initialDuration,
|
|
this.trainingId,
|
|
});
|
|
@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: _duration < 300 ? () => setState(() => _duration += 15) : null,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
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,
|
|
'trainingId': widget.trainingId,
|
|
});
|
|
}
|
|
: null,
|
|
child: const Text('Speichern'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|