wesal/frontend/lib/screens/pages/profile_page.dart

853 lines
27 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async';
import '../../services/notification_service.dart';
import '../../services/user_service.dart';
import '../../services/auth_service.dart';
import '../../services/post_service.dart';
import '../../services/app_lifecycle_service.dart';
import '../../models/post_models.dart';
import '../../widgets/posts_list.dart';
import '../../utils/password_generator.dart';
import '../edit_profile_screen.dart';
import '../support_screen.dart';
import '../information_screen.dart';
import '../notifications_settings_screen.dart';
import '../create_invitation_screen.dart';
class ProfilePage extends StatefulWidget {
@override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
String? fcmToken;
final TextEditingController _tokenController = TextEditingController();
bool isLoading = false;
Map<String, dynamic>? userData;
bool isLoadingUser = true;
int totalLikes = 0;
int totalPosts = 0;
StreamSubscription<Map<String, dynamic>>? _userStreamSubscription;
@override
void initState() {
super.initState();
_loadFCMToken();
_loadUserData();
_loadUserStats();
_setupUserStream();
}
@override
void dispose() {
_tokenController.dispose();
_userStreamSubscription?.cancel();
super.dispose();
}
void _setupUserStream() {
print('Setting up user profile stream');
_userStreamSubscription = UserService.getUserStream().listen(
(updatedUserData) {
print('📱 User profile stream received data update');
if (mounted) {
// Update UI directly with stream data instead of making API call
setState(() {
userData = updatedUserData;
isLoadingUser = false;
});
// Only reload stats which are from posts service
_loadUserStats();
}
},
onError: (error) {
print('User profile stream error: $error');
},
);
// Trigger stream with any existing cached data
Future.delayed(Duration(milliseconds: 100), () {
if (mounted) {
UserService.notifyStreamWithCurrentData();
}
});
}
Future<void> _loadFCMToken() async {
setState(() {
isLoading = true;
});
try {
final token = await NotificationService().getToken();
setState(() {
fcmToken = token;
_tokenController.text = token ?? 'Token not available';
isLoading = false;
});
} catch (e) {
setState(() {
fcmToken = null;
_tokenController.text = 'Error loading token: $e';
isLoading = false;
});
}
}
Future<void> _loadUserData({bool forceRefresh = false}) async {
// Don't show loading for cached data unless forcing refresh
final isInitialLoad = userData == null;
if (isInitialLoad || forceRefresh) {
setState(() {
isLoadingUser = true;
});
}
final result = await UserService.getCurrentUser(forceRefresh: forceRefresh);
if (mounted) {
setState(() {
if (result['success'] == true) {
userData = result['data'];
} else {
userData = null;
if (isInitialLoad) {
_showErrorAlert(result['message'] ?? 'Failed to load user data');
}
}
isLoadingUser = false;
});
}
}
Future<void> _loadUserStats() async {
final result = await PostService.getUserPosts();
if (mounted) {
setState(() {
if (result['success'] == true) {
final posts = result['posts'] as List<Post>;
totalPosts = posts.length;
totalLikes = posts.fold(0, (sum, post) => sum + post.likes);
} else {
totalPosts = 0;
totalLikes = 0;
}
});
}
}
void _showErrorAlert(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK', style: TextStyle(color: Color(0xFF6A4C93))),
),
],
),
);
}
Future<void> _copyToClipboard() async {
if (fcmToken != null && fcmToken!.isNotEmpty) {
await Clipboard.setData(ClipboardData(text: fcmToken!));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('FCM Token copied to clipboard'),
backgroundColor: Color(0xFF6A4C93),
),
);
}
}
void _navigateToSettings() {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => SettingsPage()));
}
void _navigateToEditProfile() async {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => EditProfileScreen(userData: userData),
),
);
if (result == true) {
// Profile was updated, refresh user data
_loadUserData(forceRefresh: true);
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false, // Prevent back navigation from profile page
onPopInvoked: (bool didPop) {
// Prevent any pop behavior, including iOS back gesture
if (!didPop) {
// Do nothing - stay on profile page
}
},
child: Scaffold(
appBar: AppBar(
title: Text('Profile', style: TextStyle(fontWeight: FontWeight.w600)),
backgroundColor: Colors.white,
foregroundColor: Color(0xFF6A4C93),
elevation: 0,
centerTitle: true,
actions: [
if (userData != null)
IconButton(
onPressed: _navigateToEditProfile,
icon: Icon(Icons.edit),
tooltip: 'Edit Profile',
),
IconButton(
onPressed: _navigateToSettings,
icon: Icon(Icons.settings),
),
],
bottom: PreferredSize(
preferredSize: Size.fromHeight(1),
child: Container(height: 1, color: Colors.grey[200]),
),
automaticallyImplyLeading: false,
),
body: SingleChildScrollView(
child: Column(
children: [
Container(
padding: EdgeInsets.all(24),
child: Column(
children: [
if (isLoadingUser)
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFF6A4C93),
),
)
else if (userData != null) ...[
UserAvatar(
displayName: userData!['displayName'] ?? 'Unknown User',
avatarUrl: userData!['avatar']?.toString(),
radius: 50,
),
SizedBox(height: 16),
Text(
userData!['displayName'] ?? 'Unknown User',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 4),
Text(
'@${userData!['username'] ?? 'unknown'}',
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
] else ...[
Icon(
Icons.error_outline,
size: 50,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
'Failed to load user data',
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
SizedBox(height: 8),
ElevatedButton(
onPressed: _loadUserData,
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
),
child: Text('Retry'),
),
],
SizedBox(height: 20),
if (userData != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
children: [
Text(
'$totalPosts',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF6A4C93),
),
),
Text(
'Posts',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
Column(
children: [
Text(
'$totalLikes',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF6A4C93),
),
),
Text(
'Likes Received',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
],
),
],
),
),
if (userData != null) ...[
Container(
height: 1,
color: Colors.grey[200],
margin: EdgeInsets.symmetric(horizontal: 16),
),
SizedBox(height: 16),
ProfilePostsList(
fetchPosts: () => PostService.getUserPosts(),
onStatsUpdate: (posts, likes) {
setState(() {
totalPosts = posts;
totalLikes = likes;
});
},
),
],
],
),
),
),
);
}
}
class SettingsPage extends StatefulWidget {
@override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
// Admin user creation
Map<String, dynamic>? userData;
bool isLoadingUser = true;
bool isCreatingUser = false;
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _displayNameController = TextEditingController();
@override
void initState() {
super.initState();
_loadUserData();
_generatePassword();
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_displayNameController.dispose();
super.dispose();
}
Future<void> _loadUserData() async {
setState(() {
isLoadingUser = true;
});
final result = await UserService.getCurrentUser();
setState(() {
isLoadingUser = false;
if (result['success'] == true) {
userData = result['data'];
} else {
userData = null;
}
});
}
void _generatePassword() {
_passwordController.text = PasswordGenerator.generateReadablePassword();
}
bool get _isAdmin {
return userData?['role'] == 'ADMIN';
}
Future<Map<String, dynamic>> _createUserAccount() async {
if (_emailController.text.trim().isEmpty ||
_displayNameController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please fill in all fields'),
backgroundColor: Colors.red,
),
);
return {'success': false, 'message': 'Please fill in all fields'};
}
setState(() {
isCreatingUser = true;
});
try {
final result = await UserService.createUser(
email: _emailController.text.trim(),
password: _passwordController.text,
displayName: _displayNameController.text.trim(),
);
setState(() {
isCreatingUser = false;
});
return result;
} catch (e) {
setState(() {
isCreatingUser = false;
});
return {'success': false, 'message': 'Error creating user: $e'};
}
}
void _signOut() async {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Sign Out'),
content: Text('Are you sure you want to sign out?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel'),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
// Stop polling services before logout
AppLifecycleService.dispose();
await AuthService.logout();
Navigator.of(
context,
).pushNamedAndRemoveUntil('/', (route) => false);
},
child: Text('Sign Out', style: TextStyle(color: Colors.red)),
),
],
);
},
);
}
void _showCredentialsDialog(String email, String password) {
final credentialsText = 'Email: $email\nPassword: $password';
final TextEditingController credentialsController = TextEditingController(
text: credentialsText,
);
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text('Account Created Successfully!'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Share these credentials with the user:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
SizedBox(height: 16),
TextField(
controller: credentialsController,
decoration: InputDecoration(
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(Icons.copy),
onPressed: () async {
await Clipboard.setData(
ClipboardData(text: credentialsText),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Credentials copied to clipboard'),
backgroundColor: Color(0xFF6A4C93),
),
);
},
tooltip: 'Copy credentials',
),
),
maxLines: 2,
readOnly: true,
style: TextStyle(fontFamily: 'monospace', fontSize: 14),
),
],
),
actions: [
ElevatedButton(
onPressed: () => Navigator.of(dialogContext).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
),
child: Text('Done'),
),
],
);
},
);
}
void _showCreateAccountDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: Text('Create Account'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 16),
TextField(
controller: _displayNameController,
decoration: InputDecoration(
labelText: 'Display Name',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
),
SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
setDialogState(() {
_generatePassword();
});
},
tooltip: 'Generate new password',
),
),
readOnly: true,
style: TextStyle(
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'Password is auto-generated for security',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text('Cancel'),
),
ElevatedButton(
onPressed: isCreatingUser
? null
: () async {
final email = _emailController.text.trim();
final password = _passwordController.text;
final result = await _createUserAccount();
if (mounted) {
Navigator.of(dialogContext).pop();
if (result['success']) {
_showCredentialsDialog(email, password);
// Clear form
_emailController.clear();
_displayNameController.clear();
_generatePassword();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
result['message'] ??
'Failed to create user',
),
backgroundColor: Colors.red,
),
);
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
),
child: isCreatingUser
? SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Text('Create'),
),
],
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: true, // Allow back navigation to go back to profile page only
child: Scaffold(
appBar: AppBar(
title: Text('Settings', style: TextStyle(fontWeight: FontWeight.w600)),
backgroundColor: Colors.white,
foregroundColor: Color(0xFF6A4C93),
elevation: 0,
bottom: PreferredSize(
preferredSize: Size.fromHeight(1),
child: Container(height: 1, color: Colors.grey[200]),
),
automaticallyImplyLeading: true,
),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Your Preferences',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF6A4C93),
),
),
SizedBox(height: 16),
Container(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => EditProfileScreen(userData: userData),
),
),
icon: Icon(Icons.edit, size: 20),
label: Text('Profile'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(horizontal: 16),
),
),
),
SizedBox(height: 16),
Container(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => NotificationsSettingsScreen(),
),
),
icon: Icon(Icons.notifications_outlined, size: 20),
label: Text('Notifications'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(horizontal: 16),
),
),
),
SizedBox(height: 32),
if (!isLoadingUser && _isAdmin) ...[
Text(
'Admin Tools',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF6A4C93),
),
),
SizedBox(height: 16),
Container(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (context) => CreateInvitationScreen()),
),
icon: Icon(Icons.confirmation_number_outlined, size: 20),
label: Text('Create Invitation Code'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(horizontal: 16),
),
),
),
SizedBox(height: 32),
],
Text(
'App Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF6A4C93),
),
),
SizedBox(height: 16),
Container(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (context) => InformationScreen()),
),
icon: Icon(Icons.info_outline, size: 20),
label: Text('Information'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(horizontal: 16),
),
),
),
SizedBox(height: 16),
Container(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SupportScreen()),
),
icon: Icon(Icons.help_outline, size: 20),
label: Text('Support'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(horizontal: 16),
),
),
),
SizedBox(height: 32),
Container(
width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: _signOut,
icon: Icon(Icons.logout, color: Colors.red, size: 20),
label: Text('Sign Out', style: TextStyle(color: Colors.red)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
side: BorderSide(color: Colors.red),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(horizontal: 16),
),
),
),
],
),
),
),
);
}
}