Trainer können Trainings erstellen/löschen
parent
34e959e5ca
commit
cc5e51b9ec
|
|
@ -2,13 +2,15 @@
|
||||||
// Einstiegspunkt der App und globale Konfigurationen
|
// Einstiegspunkt der App und globale Konfigurationen
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import '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';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
await Firebase.initializeApp(
|
||||||
|
options: DefaultFirebaseOptions.currentPlatform,
|
||||||
|
);
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,10 +39,10 @@ class _MyAppState extends State<MyApp> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Trainer App',
|
title: 'TrainerBox',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink),
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
home: _loggedIn
|
home: _loggedIn
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
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:firebase_auth/firebase_auth.dart';
|
||||||
|
|
||||||
class CalendarTab extends StatefulWidget {
|
class CalendarTab extends StatefulWidget {
|
||||||
const CalendarTab({super.key});
|
const CalendarTab({super.key});
|
||||||
|
|
@ -9,165 +11,373 @@ class CalendarTab extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CalendarTabState extends State<CalendarTab> {
|
class _CalendarTabState extends State<CalendarTab> {
|
||||||
CalendarFormat _calendarFormat = CalendarFormat.month;
|
CalendarFormat _calendarFormat = CalendarFormat.week;
|
||||||
DateTime _focusedDay = DateTime.now();
|
late DateTime _focusedDay;
|
||||||
DateTime? _selectedDay;
|
DateTime? _selectedDay;
|
||||||
Map<DateTime, List<Map<String, dynamic>>> _events = {};
|
Map<DateTime, List<Map<String, dynamic>>> _events = {};
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _currentUserId;
|
||||||
|
String? _userRole;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedDay = DateTime.now();
|
_focusedDay = DateTime.now();
|
||||||
final now = DateTime.now();
|
_selectedDay = _focusedDay;
|
||||||
_focusedDay = DateTime(now.year, now.month, now.day);
|
_currentUserId = FirebaseAuth.instance.currentUser?.uid;
|
||||||
|
_initializeData();
|
||||||
|
}
|
||||||
|
|
||||||
// Beispiel-Events
|
Future<void> _initializeData() async {
|
||||||
_events = {
|
if (_currentUserId != null) {
|
||||||
_focusedDay: [
|
final userDoc = await FirebaseFirestore.instance
|
||||||
{
|
.collection('User')
|
||||||
'title': 'Ganzkörper Workout',
|
.doc(_currentUserId)
|
||||||
'time': '09:00',
|
.get();
|
||||||
'duration': '45 Minuten',
|
|
||||||
},
|
if (userDoc.exists) {
|
||||||
{'title': 'Yoga Session', 'time': '17:30', 'duration': '30 Minuten'},
|
setState(() {
|
||||||
],
|
_userRole = userDoc.data()?['role'] as String?;
|
||||||
_focusedDay.add(const Duration(days: 1)): [
|
});
|
||||||
{'title': 'HIIT Training', 'time': '08:00', 'duration': '30 Minuten'},
|
await _loadEvents();
|
||||||
],
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadEvents() async {
|
||||||
|
if (_userRole == null) return;
|
||||||
|
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
QuerySnapshot trainersSnapshot;
|
||||||
|
|
||||||
|
if (_userRole == 'trainer') {
|
||||||
|
// Trainer sieht nur seine eigenen Trainings
|
||||||
|
trainersSnapshot = await FirebaseFirestore.instance
|
||||||
|
.collection('User')
|
||||||
|
.where('role', isEqualTo: 'trainer')
|
||||||
|
.where(FieldPath.documentId, isEqualTo: _currentUserId)
|
||||||
|
.get();
|
||||||
|
} else {
|
||||||
|
// Spieler sehen alle Trainings
|
||||||
|
trainersSnapshot = await FirebaseFirestore.instance
|
||||||
|
.collection('User')
|
||||||
|
.where('role', isEqualTo: 'trainer')
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
final events = <DateTime, List<Map<String, dynamic>>>{};
|
||||||
|
|
||||||
|
for (var trainerDoc in trainersSnapshot.docs) {
|
||||||
|
final trainerData = trainerDoc.data() as Map<String, dynamic>;
|
||||||
|
final trainingTimes = trainerData['trainingTimes'] as Map<String, dynamic>? ?? {};
|
||||||
|
final trainingDurations = trainerData['trainingDurations'] as Map<String, dynamic>? ?? {};
|
||||||
|
final cancelledTrainings = trainerData['cancelledTrainings'] as List<dynamic>? ?? [];
|
||||||
|
|
||||||
|
trainingTimes.forEach((day, timeStr) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final daysUntilNext = _getDaysUntilNext(day, now.weekday);
|
||||||
|
final eventDate = DateTime(now.year, now.month, now.day + daysUntilNext, hour, minute);
|
||||||
|
|
||||||
|
// Erstelle Trainings für ein ganzes Jahr
|
||||||
|
for (var i = 0; i < 52; i++) {
|
||||||
|
final date = eventDate.add(Duration(days: i * 7));
|
||||||
|
final normalizedDate = DateTime(date.year, date.month, date.day);
|
||||||
|
|
||||||
|
// Prüfe, ob das Training an diesem Tag abgesagt wurde
|
||||||
|
final isCancelled = cancelledTrainings.any((cancelled) {
|
||||||
|
if (cancelled is Map<String, dynamic>) {
|
||||||
|
final cancelledDate = DateTime.parse(cancelled['date'] as String);
|
||||||
|
return isSameDay(cancelledDate, normalizedDate);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isCancelled) {
|
||||||
|
final event = {
|
||||||
|
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer',
|
||||||
|
'time': timeStr,
|
||||||
|
'duration': duration,
|
||||||
|
'trainerId': trainerDoc.id,
|
||||||
|
'isCurrentUser': trainerDoc.id == _currentUserId,
|
||||||
|
'day': day,
|
||||||
|
'date': normalizedDate.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (events.containsKey(normalizedDate)) {
|
||||||
|
events[normalizedDate]!.add(event);
|
||||||
|
} else {
|
||||||
|
events[normalizedDate] = [event];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_events = events;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading events: $e');
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []);
|
||||||
|
|
||||||
|
// Füge das Training zur Liste der abgesagten Trainings hinzu
|
||||||
|
cancelledTrainings.add({
|
||||||
|
'date': event['date'],
|
||||||
|
'day': event['day'],
|
||||||
|
'time': event['time'],
|
||||||
|
'duration': event['duration'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aktualisiere die Daten in Firestore
|
||||||
|
await FirebaseFirestore.instance
|
||||||
|
.collection('User')
|
||||||
|
.doc(_currentUserId)
|
||||||
|
.update({
|
||||||
|
'cancelledTrainings': cancelledTrainings,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lade die Events neu
|
||||||
|
await _loadEvents();
|
||||||
|
|
||||||
|
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')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
List<Map<String, dynamic>> _getEventsForDay(DateTime day) {
|
||||||
return _events[DateTime(day.year, day.month, day.day)] ?? [];
|
final normalizedDate = DateTime(day.year, day.month, day.day);
|
||||||
|
return _events[normalizedDate] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final firstDay = DateTime.utc(1900, 1, 1);
|
|
||||||
final lastDay = DateTime.utc(2999, 12, 31);
|
|
||||||
|
|
||||||
if (_focusedDay.isBefore(firstDay)) {
|
|
||||||
_focusedDay = firstDay;
|
|
||||||
} else if (_focusedDay.isAfter(lastDay)) {
|
|
||||||
_focusedDay = lastDay;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Trainingsplan'),
|
title: const Text('Kalender'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.refresh),
|
||||||
onPressed: () {
|
onPressed: _isLoading ? null : _loadEvents,
|
||||||
// TODO: Implement add workout to calendar
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: SafeArea(
|
||||||
children: [
|
child: Column(
|
||||||
TableCalendar(
|
children: [
|
||||||
firstDay: firstDay,
|
Container(
|
||||||
lastDay: lastDay,
|
padding: const EdgeInsets.all(8.0),
|
||||||
focusedDay: _focusedDay,
|
child: TableCalendar(
|
||||||
calendarFormat: _calendarFormat,
|
firstDay: DateTime.utc(2024, 1, 1),
|
||||||
selectedDayPredicate: (day) {
|
lastDay: DateTime.utc(2025, 12, 31),
|
||||||
return isSameDay(_selectedDay, day);
|
focusedDay: _focusedDay,
|
||||||
},
|
calendarFormat: _calendarFormat,
|
||||||
onDaySelected: (selectedDay, focusedDay) {
|
selectedDayPredicate: (day) {
|
||||||
setState(() {
|
return isSameDay(_selectedDay, day);
|
||||||
_selectedDay = selectedDay;
|
},
|
||||||
_focusedDay = focusedDay;
|
onDaySelected: (selectedDay, focusedDay) {
|
||||||
});
|
setState(() {
|
||||||
},
|
_selectedDay = selectedDay;
|
||||||
onFormatChanged: (format) {
|
_focusedDay = focusedDay;
|
||||||
setState(() {
|
});
|
||||||
_calendarFormat = format;
|
},
|
||||||
});
|
onFormatChanged: (format) {
|
||||||
},
|
setState(() {
|
||||||
onPageChanged: (focusedDay) {
|
_calendarFormat = format;
|
||||||
setState(() {
|
});
|
||||||
_focusedDay = focusedDay;
|
},
|
||||||
});
|
onPageChanged: (focusedDay) {
|
||||||
},
|
setState(() {
|
||||||
eventLoader: _getEventsForDay,
|
_focusedDay = focusedDay;
|
||||||
calendarStyle: const CalendarStyle(
|
});
|
||||||
markersMaxCount: 1,
|
},
|
||||||
markerDecoration: BoxDecoration(
|
eventLoader: _getEventsForDay,
|
||||||
color: Colors.blue,
|
calendarStyle: const CalendarStyle(
|
||||||
shape: BoxShape.circle,
|
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',
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Divider(),
|
||||||
const Divider(),
|
Expanded(
|
||||||
Expanded(
|
child: _isLoading
|
||||||
child:
|
? const Center(child: CircularProgressIndicator())
|
||||||
_getEventsForDay(_selectedDay!).isEmpty
|
: _selectedDay == null
|
||||||
? const Center(
|
? const Center(child: Text('Bitte wähle einen Tag aus'))
|
||||||
child: Column(
|
: ListView.builder(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
children: [
|
itemCount: _getEventsForDay(_selectedDay!).length,
|
||||||
Icon(Icons.event_busy, size: 64, color: Colors.grey),
|
itemBuilder: (context, index) {
|
||||||
SizedBox(height: 16),
|
final event = _getEventsForDay(_selectedDay!)[index];
|
||||||
Text(
|
final isCurrentUser = event['isCurrentUser'] as bool;
|
||||||
'Keine Trainings geplant',
|
return Card(
|
||||||
style: TextStyle(fontSize: 18, color: Colors.grey),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
),
|
color: isCurrentUser ? Colors.blue.withOpacity(0.1) : null,
|
||||||
SizedBox(height: 8),
|
child: ListTile(
|
||||||
Text(
|
leading: Icon(
|
||||||
'Füge Trainings zu deinem Kalender hinzu',
|
Icons.sports,
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
color: isCurrentUser ? Colors.blue : null,
|
||||||
),
|
),
|
||||||
],
|
title: Text(
|
||||||
),
|
isCurrentUser ? 'Training' : event['trainerName'],
|
||||||
)
|
style: TextStyle(
|
||||||
: ListView.builder(
|
fontWeight: isCurrentUser ? FontWeight.bold : null,
|
||||||
padding: const EdgeInsets.all(16),
|
),
|
||||||
itemCount: _getEventsForDay(_selectedDay!).length,
|
),
|
||||||
itemBuilder: (context, index) {
|
subtitle: Text(
|
||||||
final event = _getEventsForDay(_selectedDay!)[index];
|
'${event['time']} - ${event['duration']} Minuten',
|
||||||
return Card(
|
),
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
trailing: Row(
|
||||||
child: ListTile(
|
mainAxisSize: MainAxisSize.min,
|
||||||
leading: Container(
|
children: [
|
||||||
width: 48,
|
IconButton(
|
||||||
height: 48,
|
icon: const Icon(Icons.info),
|
||||||
decoration: BoxDecoration(
|
onPressed: () {
|
||||||
color: Colors.blue[100],
|
showDialog(
|
||||||
borderRadius: BorderRadius.circular(8),
|
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)
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
);
|
||||||
Icons.fitness_center,
|
},
|
||||||
color: Colors.blue,
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
title: Text(
|
),
|
||||||
event['title'],
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
'${event['time']} • ${event['duration']}',
|
|
||||||
),
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Icons.delete_outline),
|
|
||||||
onPressed: () {
|
|
||||||
// TODO: Implement delete workout from calendar
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () {
|
|
||||||
// TODO: Implement add workout to calendar
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,179 +2,308 @@ 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';
|
||||||
|
|
||||||
class ProfileTab extends StatelessWidget {
|
class ProfileTab extends StatefulWidget {
|
||||||
final VoidCallback? onLogoutSuccess;
|
final VoidCallback? onLogoutSuccess;
|
||||||
const ProfileTab({super.key, this.onLogoutSuccess});
|
const ProfileTab({super.key, this.onLogoutSuccess});
|
||||||
|
|
||||||
Future<void> _logout(BuildContext context) async {
|
@override
|
||||||
await FirebaseAuth.instance.signOut();
|
State<ProfileTab> createState() => _ProfileTabState();
|
||||||
if (onLogoutSuccess != null) {
|
}
|
||||||
onLogoutSuccess!();
|
|
||||||
|
class _ProfileTabState extends State<ProfileTab> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
bool _isTrainer = false;
|
||||||
|
bool _isLoading = false;
|
||||||
|
Map<String, TimeOfDay> _trainingTimes = {};
|
||||||
|
Map<String, int> _trainingDurations = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadUserData();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadUserData() async {
|
||||||
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
|
if (user == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final doc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get();
|
||||||
|
if (doc.exists) {
|
||||||
|
final data = doc.data()!;
|
||||||
|
setState(() {
|
||||||
|
_isTrainer = data['role'] == 'trainer';
|
||||||
|
if (_isTrainer) {
|
||||||
|
_trainingTimes = Map<String, TimeOfDay>.from(
|
||||||
|
(data['trainingTimes'] ?? {}).map(
|
||||||
|
(key, value) => MapEntry(
|
||||||
|
key,
|
||||||
|
TimeOfDay(
|
||||||
|
hour: int.parse(value.split(':')[0]),
|
||||||
|
minute: int.parse(value.split(':')[1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_trainingDurations = Map<String, int>.from(data['trainingDurations'] ?? {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading user data: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveTrainingTime(String day, TimeOfDay? time, int? duration) async {
|
||||||
|
if (time == null || duration == null) return;
|
||||||
|
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
|
if (user == null) return;
|
||||||
|
|
||||||
|
final timeString = '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
await FirebaseFirestore.instance.collection('User').doc(user.uid).update({
|
||||||
|
'trainingTimes.$day': timeString,
|
||||||
|
'trainingDurations.$day': duration,
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_trainingTimes[day] = time;
|
||||||
|
_trainingDurations[day] = duration;
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Trainingszeit gespeichert')),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Fehler beim Speichern: $e')),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _removeTrainingTime(String day) async {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
|
if (user == null) return;
|
||||||
|
|
||||||
|
await FirebaseFirestore.instance.collection('User').doc(user.uid).update({
|
||||||
|
'trainingTimes.$day': FieldValue.delete(),
|
||||||
|
'trainingDurations.$day': FieldValue.delete(),
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_trainingTimes.remove(day);
|
||||||
|
_trainingDurations.remove(day);
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Trainingszeit entfernt')),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Fehler beim Entfernen: $e')),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectTime(BuildContext context, String day) async {
|
||||||
|
final TimeOfDay? picked = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: _trainingTimes[day] ?? TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
final duration = await showDialog<int>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _DurationDialog(
|
||||||
|
initialDuration: _trainingDurations[day] ?? 60,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (duration != null) {
|
||||||
|
await _saveTrainingTime(day, picked, duration);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
final email = user?.email ?? 'Keine E-Mail gefunden';
|
if (user == null) {
|
||||||
final uid = user?.uid;
|
return const Center(child: Text('Nicht eingeloggt'));
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: uid == null
|
appBar: AppBar(
|
||||||
? const Center(child: Text('Nicht eingeloggt'))
|
title: const Text('Profil'),
|
||||||
: FutureBuilder<DocumentSnapshot>(
|
actions: [
|
||||||
future: FirebaseFirestore.instance.collection('User').doc(uid).get(),
|
IconButton(
|
||||||
|
icon: const Icon(Icons.logout),
|
||||||
|
onPressed: () async {
|
||||||
|
await FirebaseAuth.instance.signOut();
|
||||||
|
if (widget.onLogoutSuccess != null) {
|
||||||
|
widget.onLogoutSuccess!();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Persönliche Informationen',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FutureBuilder<DocumentSnapshot>(
|
||||||
|
future: FirebaseFirestore.instance.collection('User').doc(user.uid).get(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
if (!snapshot.hasData || !snapshot.data!.exists) {
|
if (!snapshot.hasData || !snapshot.data!.exists) {
|
||||||
return const Center(child: Text('Keine Profildaten gefunden'));
|
return const Center(child: Text('Keine Daten gefunden'));
|
||||||
}
|
}
|
||||||
final data = snapshot.data!.data() as Map<String, dynamic>;
|
final data = snapshot.data!.data() as Map<String, dynamic>;
|
||||||
final name = data['name'] ?? 'Unbekannt';
|
return Column(
|
||||||
final role = data['role'] == 'trainer' ? 'Trainer' : 'Spieler';
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
final createdAt = (data['createdAt'] is Timestamp)
|
children: [
|
||||||
? (data['createdAt'] as Timestamp).toDate()
|
Text('Name: ${data['name'] ?? '-'}'),
|
||||||
: null;
|
const SizedBox(height: 8),
|
||||||
final createdAtStr = createdAt != null
|
Text('E-Mail: ${user.email ?? '-'}'),
|
||||||
? '${createdAt.day.toString().padLeft(2, '0')}.${createdAt.month.toString().padLeft(2, '0')}.${createdAt.year}'
|
const SizedBox(height: 8),
|
||||||
: 'Unbekannt';
|
Text('Rolle: ${data['role'] ?? '-'}'),
|
||||||
// Beispiel-Benutzerdaten (Statistiken als Demo)
|
if (_isTrainer) ...[
|
||||||
final Map<String, dynamic> userData = {
|
const SizedBox(height: 24),
|
||||||
'name': name,
|
const Text(
|
||||||
'level': role,
|
'Trainingszeiten',
|
||||||
'joinedDate': createdAtStr,
|
style: TextStyle(
|
||||||
'workoutsCompleted': 42,
|
fontSize: 18,
|
||||||
'totalMinutes': 1260,
|
fontWeight: FontWeight.bold,
|
||||||
};
|
|
||||||
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverAppBar(
|
|
||||||
expandedHeight: 200,
|
|
||||||
pinned: true,
|
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
|
||||||
title: Text(userData['name']),
|
|
||||||
background: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Colors.blue[700]!, Colors.blue[500]!],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(Icons.person, size: 80, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
SliverToBoxAdapter(
|
...['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
|
||||||
child: Padding(
|
.map((day) => Card(
|
||||||
padding: const EdgeInsets.all(16.0),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
child: Column(
|
child: ListTile(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
title: Text(day),
|
||||||
children: [
|
subtitle: _trainingTimes[day] != null
|
||||||
_buildInfoCard(
|
? Text(
|
||||||
title: 'Persönliche Informationen',
|
'${_trainingTimes[day]!.format(context)} - ${_trainingDurations[day]} Minuten',
|
||||||
child: Column(
|
)
|
||||||
children: [
|
: const Text('Keine Trainingszeit'),
|
||||||
_buildInfoRow('E-Mail', email, Icons.email),
|
trailing: Row(
|
||||||
const Divider(),
|
mainAxisSize: MainAxisSize.min,
|
||||||
_buildInfoRow('Name', name, Icons.person),
|
children: [
|
||||||
const Divider(),
|
IconButton(
|
||||||
_buildInfoRow('Rolle', role, Icons.badge),
|
icon: Icon(
|
||||||
const Divider(),
|
_trainingTimes[day] != null
|
||||||
_buildInfoRow(
|
? Icons.edit
|
||||||
'Mitglied seit',
|
: Icons.add,
|
||||||
userData['joinedDate'],
|
),
|
||||||
Icons.calendar_today,
|
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),
|
],
|
||||||
_buildInfoCard(
|
|
||||||
title: 'Einstellungen',
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.notifications),
|
|
||||||
title: const Text('Benachrichtigungen'),
|
|
||||||
trailing: Switch(
|
|
||||||
value: true,
|
|
||||||
onChanged: (value) {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.dark_mode),
|
|
||||||
title: const Text('Dark Mode'),
|
|
||||||
trailing: Switch(
|
|
||||||
value: false,
|
|
||||||
onChanged: (value) {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.language),
|
|
||||||
title: const Text('Sprache'),
|
|
||||||
trailing: const Text('Deutsch'),
|
|
||||||
onTap: () {},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Center(
|
|
||||||
child: TextButton.icon(
|
|
||||||
onPressed: () => _logout(context),
|
|
||||||
icon: const Icon(Icons.logout),
|
|
||||||
label: const Text('Abmelden'),
|
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
}
|
),
|
||||||
|
|
||||||
Widget _buildInfoCard({required String title, required Widget child}) {
|
|
||||||
return Card(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildInfoRow(String label, String value, IconData icon) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: Colors.blue),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(child: Text(label)),
|
|
||||||
Text(value, style: TextStyle(color: Colors.grey[600])),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _DurationDialog extends StatefulWidget {
|
||||||
|
final int initialDuration;
|
||||||
|
|
||||||
|
const _DurationDialog({required this.initialDuration});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DurationDialog> createState() => _DurationDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DurationDialogState extends State<_DurationDialog> {
|
||||||
|
late int _duration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_duration = widget.initialDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Trainingsdauer'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('Wähle die Dauer der Trainingseinheit in Minuten:'),
|
||||||
|
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: () => Navigator.pop(context, _duration),
|
||||||
|
child: const Text('Bestätigen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -316,10 +316,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
version: "0.19.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -465,10 +465,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: table_calendar
|
name: table_calendar
|
||||||
sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982"
|
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.1.3"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue