Merge pull request #12 from sBubshait/feature/profile-update
Feature/profile update
This commit is contained in:
commit
7bb6bad5c9
@ -69,18 +69,39 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/updateUser")
|
@PostMapping("/updateUser")
|
||||||
@Operation(summary = "Update user information", description = "Update authenticated user's fcmToken, displayName, avatar, or password")
|
@Operation(summary = "Update user information", description = "Update authenticated user's fcmToken, displayName, avatar, password, or username")
|
||||||
public ResponseEntity<Map<String, Object>> updateUser(@Valid @RequestBody UpdateUserRequest request) {
|
public ResponseEntity<Map<String, Object>> updateUser(@Valid @RequestBody UpdateUserRequest request) {
|
||||||
try {
|
try {
|
||||||
User user = userService.updateUser(
|
User user = userService.updateUser(
|
||||||
request.getFcmToken(),
|
request.getFcmToken(),
|
||||||
request.getDisplayName(),
|
request.getDisplayName(),
|
||||||
request.getAvatar(),
|
request.getAvatar(),
|
||||||
request.getPassword()
|
request.getPassword(),
|
||||||
|
request.getUsername()
|
||||||
);
|
);
|
||||||
return ResponseEntity.ok(Map.of("status", 200, "user", user));
|
|
||||||
|
// Create user data without password
|
||||||
|
Map<String, Object> userData = Map.of(
|
||||||
|
"id", user.getId(),
|
||||||
|
"email", user.getEmail(),
|
||||||
|
"username", user.getUsername() != null ? user.getUsername() : "",
|
||||||
|
"displayName", user.getDisplayName(),
|
||||||
|
"avatar", user.getAvatar() != null ? user.getAvatar() : "",
|
||||||
|
"fcmToken", user.getFcmToken() != null ? user.getFcmToken() : "",
|
||||||
|
"activated", user.isActivated(),
|
||||||
|
"role", user.getRole()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", true,
|
||||||
|
"message", "Profile updated successfully",
|
||||||
|
"data", userData
|
||||||
|
));
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("status", 400, "message", e.getMessage()));
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", false,
|
||||||
|
"message", e.getMessage()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,8 @@ public class UpdateUserRequest {
|
|||||||
|
|
||||||
private String avatar;
|
private String avatar;
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
|
||||||
@Size(min = 6, message = "Password must be at least 8 characters long")
|
@Size(min = 6, message = "Password must be at least 8 characters long")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@ -46,4 +48,12 @@ public class UpdateUserRequest {
|
|||||||
public void setPassword(String password) {
|
public void setPassword(String password) {
|
||||||
this.password = password;
|
this.password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -56,8 +56,9 @@ public class UserService {
|
|||||||
return userRepository.save(user);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User updateUser(String fcmToken, String displayName, String avatar, String password) {
|
public User updateUser(String fcmToken, String displayName, String avatar, String password, String username) {
|
||||||
User user = getCurrentUser();
|
User user = getCurrentUser();
|
||||||
|
boolean wasNotActivated = !user.isActivated();
|
||||||
|
|
||||||
if (fcmToken != null && !fcmToken.trim().isEmpty()) {
|
if (fcmToken != null && !fcmToken.trim().isEmpty()) {
|
||||||
user.setFcmToken(fcmToken);
|
user.setFcmToken(fcmToken);
|
||||||
@ -75,6 +76,23 @@ public class UserService {
|
|||||||
user.setPassword(passwordEncoder.encode(password));
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
}
|
}
|
||||||
|
|
||||||
return userRepository.save(user);
|
if (username != null && !username.trim().isEmpty()) {
|
||||||
|
// Check if username is different from current username to avoid false positives
|
||||||
|
if (!username.equals(user.getUsername()) && userRepository.existsByUsername(username)) {
|
||||||
|
System.out.println("Username taken!");
|
||||||
|
throw new RuntimeException("@" + username + " is already taken. Try another username");
|
||||||
|
}
|
||||||
|
System.out.println("It still happened here");
|
||||||
|
user.setUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
User savedUser = userRepository.save(user);
|
||||||
|
|
||||||
|
if (wasNotActivated) {
|
||||||
|
savedUser.setActivated(true);
|
||||||
|
savedUser = userRepository.save(savedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
return savedUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
11
frontend/app_info.txt
Normal file
11
frontend/app_info.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Wesal App - Connecting Colleagues
|
||||||
|
|
||||||
|
Wesal (وصال) is a social networking mobile application designed specifically for connecting colleagues and transforming workplace social interactions. The app name is Arabic for "connection" or "union."
|
||||||
|
|
||||||
|
It is a platform that enhances communication and collaboration among division members allowing them to build deeper connections and share experiences beyond formal work-related discussions.
|
||||||
|
|
||||||
|
The app was fully developed by Saleh Bubshait within the COD/DPSD/ERP Mgmt Group. However, the app was proposed by Weed Batarfi and Insijam team.
|
||||||
|
|
||||||
|
The app is being minimally maintained but is not actively developed at the moment as the developer is currently on assignment. For any help or inquiries, please reach out to the DPSD/ERP Mgmt Group.
|
||||||
|
|
||||||
|
Thank you for using Wesal!
|
||||||
16
frontend/assets/app_info.txt
Normal file
16
frontend/assets/app_info.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
Wesal App - Connecting Colleagues
|
||||||
|
|
||||||
|
Wesal (وصال) is a social networking mobile application designed specifically for connecting colleagues and transforming workplace social interactions. The app name is Arabic for "connection" or "union."
|
||||||
|
|
||||||
|
Features:
|
||||||
|
• Social feed for sharing updates and thoughts
|
||||||
|
• Event invitations and RSVP management
|
||||||
|
• Real-time notifications for engagement
|
||||||
|
• Profile management and customization
|
||||||
|
• Secure authentication and data protection
|
||||||
|
|
||||||
|
This application was developed to foster better communication and stronger relationships among team members, making workplace collaboration more enjoyable and effective.
|
||||||
|
|
||||||
|
For technical support or account-related inquiries, please contact the ERP Management Group from your official company email address.
|
||||||
|
|
||||||
|
Thank you for using Wesal!
|
||||||
@ -3,7 +3,7 @@ import 'package:firebase_core/firebase_core.dart';
|
|||||||
import 'firebase_options.dart';
|
import 'firebase_options.dart';
|
||||||
import 'screens/home_screen.dart';
|
import 'screens/home_screen.dart';
|
||||||
import 'screens/notification_permission_screen.dart';
|
import 'screens/notification_permission_screen.dart';
|
||||||
import 'services/notification_service.dart';
|
import 'screens/edit_profile_screen.dart';
|
||||||
import 'services/auth_service.dart';
|
import 'services/auth_service.dart';
|
||||||
import 'services/user_service.dart';
|
import 'services/user_service.dart';
|
||||||
import 'services/app_lifecycle_service.dart';
|
import 'services/app_lifecycle_service.dart';
|
||||||
@ -53,11 +53,25 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
final userResult = await UserService.getCurrentUser();
|
final userResult = await UserService.getCurrentUser();
|
||||||
if (userResult['success'] == true) {
|
if (userResult['success'] == true) {
|
||||||
// Start polling services now that user is logged in and going to main screen
|
final userData = userResult['data'];
|
||||||
AppLifecycleService.startAllPolling();
|
|
||||||
Navigator.of(context).pushReplacement(
|
// Check if user needs onboarding (activated = 0)
|
||||||
MaterialPageRoute(builder: (context) => HomeScreen()),
|
if (userData['activated'] == 0) {
|
||||||
);
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => EditProfileScreen(
|
||||||
|
userData: userData,
|
||||||
|
isOnboarding: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Start polling services now that user is logged in and going to main screen
|
||||||
|
AppLifecycleService.startAllPolling();
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (context) => HomeScreen()),
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await AuthService.logout();
|
await AuthService.logout();
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
@ -344,14 +358,32 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
if (result['success'] == true) {
|
if (result['success'] == true) {
|
||||||
final userResult = await UserService.getCurrentUser(forceRefresh: true);
|
final userResult = await UserService.getCurrentUser(forceRefresh: true);
|
||||||
|
|
||||||
// Start polling services now that user is logged in
|
if (userResult['success'] == true) {
|
||||||
AppLifecycleService.startAllPolling();
|
final userData = userResult['data'];
|
||||||
|
|
||||||
Navigator.of(context).pushReplacement(
|
// Check if user needs onboarding (activated = 0)
|
||||||
MaterialPageRoute(
|
if (userData['activated'] == 0) {
|
||||||
builder: (context) => NotificationPermissionScreen(),
|
Navigator.of(context).pushReplacement(
|
||||||
),
|
MaterialPageRoute(
|
||||||
);
|
builder: (context) => EditProfileScreen(
|
||||||
|
userData: userData,
|
||||||
|
isOnboarding: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Start polling services now that user is logged in
|
||||||
|
AppLifecycleService.startAllPolling();
|
||||||
|
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => NotificationPermissionScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_showErrorAlert('Failed to load user data');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_showErrorAlert(result['message'] ?? 'Login failed');
|
_showErrorAlert(result['message'] ?? 'Login failed');
|
||||||
}
|
}
|
||||||
|
|||||||
428
frontend/lib/screens/edit_profile_screen.dart
Normal file
428
frontend/lib/screens/edit_profile_screen.dart
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../services/user_service.dart';
|
||||||
|
import '../services/app_lifecycle_service.dart';
|
||||||
|
import 'notification_permission_screen.dart';
|
||||||
|
|
||||||
|
class EditProfileScreen extends StatefulWidget {
|
||||||
|
final Map<String, dynamic>? userData;
|
||||||
|
final bool isOnboarding;
|
||||||
|
|
||||||
|
const EditProfileScreen({
|
||||||
|
Key? key,
|
||||||
|
this.userData,
|
||||||
|
this.isOnboarding = false,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_EditProfileScreenState createState() => _EditProfileScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final TextEditingController _displayNameController = TextEditingController();
|
||||||
|
final TextEditingController _usernameController = TextEditingController();
|
||||||
|
final TextEditingController _emailController = TextEditingController();
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initializeFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initializeFields() {
|
||||||
|
if (widget.userData != null) {
|
||||||
|
_displayNameController.text = widget.userData!['displayName'] ?? '';
|
||||||
|
_emailController.text = widget.userData!['email'] ?? '';
|
||||||
|
if (!widget.isOnboarding) {
|
||||||
|
_usernameController.text = widget.userData!['username'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_displayNameController.dispose();
|
||||||
|
_usernameController.dispose();
|
||||||
|
_emailController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validateDisplayName(String? value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'Display name is required';
|
||||||
|
}
|
||||||
|
if (value.trim().length < 2) {
|
||||||
|
return 'Display name must be at least 2 characters';
|
||||||
|
}
|
||||||
|
if (value.trim().length > 50) {
|
||||||
|
return 'Display name cannot exceed 50 characters';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validateUsername(String? value) {
|
||||||
|
if (widget.isOnboarding) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'Username is required';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value != null && value.trim().isNotEmpty) {
|
||||||
|
if (value.trim().length < 3) {
|
||||||
|
return 'Username must be at least 3 characters';
|
||||||
|
}
|
||||||
|
if (value.trim().length > 30) {
|
||||||
|
return 'Username cannot exceed 30 characters';
|
||||||
|
}
|
||||||
|
// Basic username validation
|
||||||
|
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value.trim())) {
|
||||||
|
return 'Username can only contain letters, numbers, and underscores';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveProfile() async {
|
||||||
|
if (!_formKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Map<String, dynamic> updateData = {
|
||||||
|
'displayName': _displayNameController.text.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (widget.isOnboarding || _usernameController.text.trim().isNotEmpty) {
|
||||||
|
updateData['username'] = _usernameController.text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await UserService.updateUser(
|
||||||
|
displayName: updateData['displayName'],
|
||||||
|
username: updateData['username'],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result['success'] == true) {
|
||||||
|
_showSuccessMessage(result['message'] ?? 'Profile updated successfully');
|
||||||
|
|
||||||
|
if (widget.isOnboarding) {
|
||||||
|
// Start polling services now that user has completed onboarding
|
||||||
|
AppLifecycleService.startAllPolling();
|
||||||
|
|
||||||
|
// Navigate to notification permission screen after successful onboarding
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (context) => NotificationPermissionScreen()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Go back to profile page
|
||||||
|
Navigator.of(context).pop(true); // Return true to indicate update
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_showErrorMessage(result['message'] ?? 'Failed to update profile');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
_showErrorMessage('An error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSuccessMessage(String message) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showErrorMessage(String message) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
widget.isOnboarding ? 'Setup Profile' : 'Edit Profile',
|
||||||
|
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: !widget.isOnboarding,
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFF32B0A5),
|
||||||
|
Color(0xFF4600B9),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (widget.isOnboarding) ...[
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'وصال',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Blaka',
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: FontWeight.w200,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Complete your profile setup',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 40),
|
||||||
|
] else ...[
|
||||||
|
SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Profile Information',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Display Name Field
|
||||||
|
Text(
|
||||||
|
'Display Name',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: _displayNameController,
|
||||||
|
validator: _validateDisplayName,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Enter your display name',
|
||||||
|
prefixIcon: Icon(Icons.person, color: Color(0xFF6A4C93)),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Color(0xFF6A4C93), width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.red, width: 2),
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Email Field (Read-only)
|
||||||
|
Text(
|
||||||
|
'Email',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: _emailController,
|
||||||
|
readOnly: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Email address',
|
||||||
|
prefixIcon: Icon(Icons.email, color: Colors.grey[400]),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey[50],
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
),
|
||||||
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Email cannot be changed',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Username Field
|
||||||
|
Text(
|
||||||
|
'Username${widget.isOnboarding ? ' *' : ''}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: _usernameController,
|
||||||
|
validator: _validateUsername,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: widget.isOnboarding
|
||||||
|
? 'Choose a username'
|
||||||
|
: 'Enter your username (optional)',
|
||||||
|
prefixIcon: Icon(Icons.alternate_email, color: Color(0xFF6A4C93)),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Color(0xFF6A4C93), width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.red, width: 2),
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Username can contain letters, numbers, and underscores only',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _saveProfile,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Color(0xFF6A4C93),
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
child: _isLoading
|
||||||
|
? SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6A4C93)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
widget.isOnboarding ? 'Complete Setup' : 'Save Changes',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
181
frontend/lib/screens/information_screen.dart
Normal file
181
frontend/lib/screens/information_screen.dart
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class InformationScreen extends StatefulWidget {
|
||||||
|
const InformationScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_InformationScreenState createState() => _InformationScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InformationScreenState extends State<InformationScreen> {
|
||||||
|
String appInfo = '';
|
||||||
|
bool isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadAppInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAppInfo() async {
|
||||||
|
try {
|
||||||
|
final String info = await rootBundle.loadString('assets/app_info.txt');
|
||||||
|
setState(() {
|
||||||
|
appInfo = info;
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading app info: $e');
|
||||||
|
setState(() {
|
||||||
|
appInfo = 'Unable to load app information.';
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
'Information',
|
||||||
|
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]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [Color(0xFF32B0A5), Color(0xFF4600B9)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 20),
|
||||||
|
|
||||||
|
// App Logo
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
child: Text(
|
||||||
|
'وصال',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Blaka',
|
||||||
|
fontSize: 80,
|
||||||
|
fontWeight: FontWeight.w200,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(2, 2),
|
||||||
|
blurRadius: 4,
|
||||||
|
color: Colors.black.withOpacity(0.3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 20),
|
||||||
|
|
||||||
|
// App Version
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Version 1.0.0',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 40),
|
||||||
|
|
||||||
|
// App Information
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white.withOpacity(0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: isLoading
|
||||||
|
? Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
appInfo,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
height: 1.6,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Additional info
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Color(0xFF6A4C93),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 4,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Back to Settings',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -80,165 +80,166 @@ class _NotificationPermissionScreenState
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: false, // Prevent back navigation from notification permission screen
|
canPop:
|
||||||
|
false, // Prevent back navigation from notification permission screen
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [Color(0xFF32B0A5), Color(0xFF4600B9)],
|
colors: [Color(0xFF32B0A5), Color(0xFF4600B9)],
|
||||||
stops: [0.0, 0.5],
|
stops: [0.0, 0.5],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
child: SafeArea(
|
||||||
child: SafeArea(
|
child: Padding(
|
||||||
child: Padding(
|
padding: EdgeInsets.all(24),
|
||||||
padding: EdgeInsets.all(24),
|
child: Column(
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
Expanded(
|
||||||
Expanded(
|
child: Column(
|
||||||
child: Column(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.rocket_launch,
|
||||||
|
size: 100,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 40),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'All Set!',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'But one last thing...',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 40),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white.withOpacity(0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.notifications_active,
|
||||||
|
size: 40,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Stay Connected',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'We will only send you updates on your own invitations or posts. You can always change your notification settings later.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.white.withOpacity(0.8),
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 200,
|
width: double.infinity,
|
||||||
height: 200,
|
height: 56,
|
||||||
decoration: BoxDecoration(
|
child: ElevatedButton(
|
||||||
color: Colors.white.withOpacity(0.2),
|
onPressed: _isLoading
|
||||||
shape: BoxShape.circle,
|
? null
|
||||||
),
|
: _requestNotificationPermission,
|
||||||
child: Icon(
|
style: ElevatedButton.styleFrom(
|
||||||
Icons.rocket_launch,
|
backgroundColor: Colors.white,
|
||||||
size: 100,
|
foregroundColor: Color(0xFF6A4C93),
|
||||||
color: Colors.white,
|
shape: RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
|
elevation: 4,
|
||||||
SizedBox(height: 40),
|
),
|
||||||
|
child: _isLoading
|
||||||
Text(
|
? CircularProgressIndicator(
|
||||||
'All Set!',
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
style: TextStyle(
|
Color(0xFF6A4C93),
|
||||||
fontSize: 32,
|
),
|
||||||
fontWeight: FontWeight.bold,
|
)
|
||||||
color: Colors.white,
|
: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.notifications, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Enable Notifications',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
|
|
||||||
Text(
|
TextButton(
|
||||||
'But one last thing...',
|
onPressed: _isLoading ? null : _skipNotifications,
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontSize: 18,
|
'Skip for now',
|
||||||
color: Colors.white.withOpacity(0.9),
|
style: TextStyle(
|
||||||
),
|
color: Colors.white.withOpacity(0.8),
|
||||||
),
|
fontSize: 16,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
SizedBox(height: 40),
|
|
||||||
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(24),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.white.withOpacity(0.3),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.notifications_active,
|
|
||||||
size: 40,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Stay Connected',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'We will only send you updates on your own invitations or posts. You can always change your notification settings later.',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: Colors.white.withOpacity(0.8),
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 56,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: _isLoading
|
|
||||||
? null
|
|
||||||
: _requestNotificationPermission,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
foregroundColor: Color(0xFF6A4C93),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
elevation: 4,
|
|
||||||
),
|
|
||||||
child: _isLoading
|
|
||||||
? CircularProgressIndicator(
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
Color(0xFF6A4C93),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.notifications, size: 20),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'Enable Notifications',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 16),
|
|
||||||
|
|
||||||
TextButton(
|
|
||||||
onPressed: _isLoading ? null : _skipNotifications,
|
|
||||||
child: Text(
|
|
||||||
'Skip for now',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white.withOpacity(0.8),
|
|
||||||
fontSize: 16,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,9 @@ import '../../services/app_lifecycle_service.dart';
|
|||||||
import '../../models/post_models.dart';
|
import '../../models/post_models.dart';
|
||||||
import '../../widgets/posts_list.dart';
|
import '../../widgets/posts_list.dart';
|
||||||
import '../../utils/password_generator.dart';
|
import '../../utils/password_generator.dart';
|
||||||
|
import '../edit_profile_screen.dart';
|
||||||
|
import '../support_screen.dart';
|
||||||
|
import '../information_screen.dart';
|
||||||
|
|
||||||
class ProfilePage extends StatefulWidget {
|
class ProfilePage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -168,6 +171,19 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
).push(MaterialPageRoute(builder: (context) => SettingsPage()));
|
).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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopScope(
|
return PopScope(
|
||||||
@ -185,6 +201,12 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
foregroundColor: Color(0xFF6A4C93),
|
foregroundColor: Color(0xFF6A4C93),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
actions: [
|
actions: [
|
||||||
|
if (userData != null)
|
||||||
|
IconButton(
|
||||||
|
onPressed: _navigateToEditProfile,
|
||||||
|
icon: Icon(Icons.edit),
|
||||||
|
tooltip: 'Edit Profile',
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: _navigateToSettings,
|
onPressed: _navigateToSettings,
|
||||||
icon: Icon(Icons.settings),
|
icon: Icon(Icons.settings),
|
||||||
@ -338,10 +360,6 @@ class SettingsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SettingsPageState extends State<SettingsPage> {
|
class _SettingsPageState extends State<SettingsPage> {
|
||||||
String? fcmToken;
|
|
||||||
final TextEditingController _tokenController = TextEditingController();
|
|
||||||
bool isLoading = false;
|
|
||||||
|
|
||||||
// Admin user creation
|
// Admin user creation
|
||||||
Map<String, dynamic>? userData;
|
Map<String, dynamic>? userData;
|
||||||
bool isLoadingUser = true;
|
bool isLoadingUser = true;
|
||||||
@ -353,52 +371,18 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadFCMToken();
|
|
||||||
_loadUserData();
|
_loadUserData();
|
||||||
_generatePassword();
|
_generatePassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tokenController.dispose();
|
|
||||||
_emailController.dispose();
|
_emailController.dispose();
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
_displayNameController.dispose();
|
_displayNameController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
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> _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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadUserData() async {
|
Future<void> _loadUserData() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -715,17 +699,18 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
|
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () => _showCreateAccountDialog(context),
|
onPressed: () => _showCreateAccountDialog(context),
|
||||||
icon: Icon(Icons.person_add, size: 18),
|
icon: Icon(Icons.person_add, size: 20),
|
||||||
label: Text('Create an Account'),
|
label: Text('Create an Account'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Color(0xFF6A4C93),
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -734,7 +719,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
],
|
],
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
'Development Tools',
|
'App Information',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -743,92 +728,88 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
|
|
||||||
Text(
|
Container(
|
||||||
'FCM Token:',
|
width: double.infinity,
|
||||||
style: TextStyle(
|
height: 56,
|
||||||
fontSize: 16,
|
child: ElevatedButton.icon(
|
||||||
fontWeight: FontWeight.w500,
|
onPressed: () => Navigator.of(context).push(
|
||||||
color: Colors.black87,
|
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: 8),
|
|
||||||
|
|
||||||
if (isLoading)
|
SizedBox(height: 16),
|
||||||
Center(
|
|
||||||
child: CircularProgressIndicator(
|
Container(
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6A4C93)),
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(builder: (context) => InformationScreen()),
|
||||||
),
|
),
|
||||||
)
|
icon: Icon(Icons.info_outline, size: 20),
|
||||||
else
|
label: Text('Information'),
|
||||||
Column(
|
style: ElevatedButton.styleFrom(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
children: [
|
foregroundColor: Colors.white,
|
||||||
Container(
|
shape: RoundedRectangleBorder(
|
||||||
decoration: BoxDecoration(
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: Colors.grey[300]!),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
controller: _tokenController,
|
|
||||||
readOnly: true,
|
|
||||||
maxLines: 4,
|
|
||||||
style: TextStyle(fontSize: 12, fontFamily: 'monospace'),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: InputBorder.none,
|
|
||||||
contentPadding: EdgeInsets.all(12),
|
|
||||||
hintText: 'FCM Token will appear here...',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
),
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: fcmToken != null ? _copyToClipboard : null,
|
|
||||||
icon: Icon(Icons.copy, size: 18),
|
|
||||||
label: Text('Copy Token'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Color(0xFF6A4C93),
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: _loadFCMToken,
|
|
||||||
icon: Icon(Icons.refresh, size: 18),
|
|
||||||
label: Text('Refresh'),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: Color(0xFF6A4C93),
|
|
||||||
side: BorderSide(color: Color(0xFF6A4C93)),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
SizedBox(height: 32),
|
SizedBox(height: 32),
|
||||||
|
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
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('Update your 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(
|
child: ElevatedButton.icon(
|
||||||
onPressed: _signOut,
|
onPressed: _signOut,
|
||||||
icon: Icon(Icons.logout, color: Colors.red),
|
icon: Icon(Icons.logout, color: Colors.red, size: 20),
|
||||||
label: Text('Sign Out', style: TextStyle(color: Colors.red)),
|
label: Text('Sign Out', style: TextStyle(color: Colors.red)),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
side: BorderSide(color: Colors.red),
|
side: BorderSide(color: Colors.red),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
163
frontend/lib/screens/support_screen.dart
Normal file
163
frontend/lib/screens/support_screen.dart
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SupportScreen extends StatelessWidget {
|
||||||
|
const SupportScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Support', 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]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFF32B0A5),
|
||||||
|
Color(0xFF4600B9),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.contact_support,
|
||||||
|
size: 60,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 40),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Need Help?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'We\'re here to help you!',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 40),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white.withOpacity(0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.email_outlined,
|
||||||
|
size: 40,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Contact Support',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'For account creation, password reset, or any other assistance, please contact ERP Management Group from your official company email address.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'ERP Management Group',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Color(0xFF6A4C93),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 4,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Back to Settings',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,12 +7,21 @@ import 'cache_service.dart';
|
|||||||
|
|
||||||
class InvitationsService {
|
class InvitationsService {
|
||||||
static bool _pollingStarted = false;
|
static bool _pollingStarted = false;
|
||||||
|
|
||||||
/// Get all invitations with caching - returns cached data if available and fresh
|
/// Get all invitations with caching - returns cached data if available and fresh
|
||||||
static Future<Map<String, dynamic>> getAllInvitations({bool forceRefresh = false}) async {
|
static Future<Map<String, dynamic>> getAllInvitations({
|
||||||
|
bool forceRefresh = false,
|
||||||
|
}) async {
|
||||||
// Return cached data if available and not forcing refresh
|
// Return cached data if available and not forcing refresh
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
final cached = CacheService.getCached<InvitationsData>(CacheService.invitationsAllKey);
|
final cached = CacheService.getCached<InvitationsData>(
|
||||||
if (cached != null && !CacheService.isCacheExpired(CacheService.invitationsAllKey, CacheService.invitationsCacheDuration)) {
|
CacheService.invitationsAllKey,
|
||||||
|
);
|
||||||
|
if (cached != null &&
|
||||||
|
!CacheService.isCacheExpired(
|
||||||
|
CacheService.invitationsAllKey,
|
||||||
|
CacheService.invitationsCacheDuration,
|
||||||
|
)) {
|
||||||
return {
|
return {
|
||||||
'success': true,
|
'success': true,
|
||||||
'message': 'Loaded from cache',
|
'message': 'Loaded from cache',
|
||||||
@ -21,7 +30,9 @@ class InvitationsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return stale data while fetching fresh data in background
|
// Return stale data while fetching fresh data in background
|
||||||
final stale = CacheService.getCachedStale<InvitationsData>(CacheService.invitationsAllKey);
|
final stale = CacheService.getCachedStale<InvitationsData>(
|
||||||
|
CacheService.invitationsAllKey,
|
||||||
|
);
|
||||||
if (stale != null && !forceRefresh) {
|
if (stale != null && !forceRefresh) {
|
||||||
// Trigger background refresh
|
// Trigger background refresh
|
||||||
_fetchAndCacheInvitations();
|
_fetchAndCacheInvitations();
|
||||||
@ -37,7 +48,9 @@ class InvitationsService {
|
|||||||
return await _fetchAndCacheInvitations();
|
return await _fetchAndCacheInvitations();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> _fetchAndCacheInvitations([bool fromPolling = false]) async {
|
static Future<Map<String, dynamic>> _fetchAndCacheInvitations([
|
||||||
|
bool fromPolling = false,
|
||||||
|
]) async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.get(
|
final response = await HttpService.get(
|
||||||
ApiConstants.getAllInvitationsEndpoint,
|
ApiConstants.getAllInvitationsEndpoint,
|
||||||
@ -52,7 +65,10 @@ class InvitationsService {
|
|||||||
|
|
||||||
// Only cache when not called from polling to prevent conflicts
|
// Only cache when not called from polling to prevent conflicts
|
||||||
if (!fromPolling) {
|
if (!fromPolling) {
|
||||||
CacheService.setCached(CacheService.invitationsAllKey, invitationsData);
|
CacheService.setCached(
|
||||||
|
CacheService.invitationsAllKey,
|
||||||
|
invitationsData,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -63,8 +79,13 @@ class InvitationsService {
|
|||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': invitationsResponse.message ?? 'Failed to fetch invitations',
|
'message':
|
||||||
'invitations': InvitationsData(created: [], accepted: [], available: []),
|
invitationsResponse.message ?? 'Failed to fetch invitations',
|
||||||
|
'invitations': InvitationsData(
|
||||||
|
created: [],
|
||||||
|
accepted: [],
|
||||||
|
available: [],
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
@ -72,13 +93,21 @@ class InvitationsService {
|
|||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': 'Session expired. Please login again.',
|
'message': 'Session expired. Please login again.',
|
||||||
'invitations': InvitationsData(created: [], accepted: [], available: []),
|
'invitations': InvitationsData(
|
||||||
|
created: [],
|
||||||
|
accepted: [],
|
||||||
|
available: [],
|
||||||
|
),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': 'Server error (${response.statusCode})',
|
'message': 'Server error (${response.statusCode})',
|
||||||
'invitations': InvitationsData(created: [], accepted: [], available: []),
|
'invitations': InvitationsData(
|
||||||
|
created: [],
|
||||||
|
accepted: [],
|
||||||
|
available: [],
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -86,7 +115,11 @@ class InvitationsService {
|
|||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': 'Network error. Please check your connection.',
|
'message': 'Network error. Please check your connection.',
|
||||||
'invitations': InvitationsData(created: [], accepted: [], available: []),
|
'invitations': InvitationsData(
|
||||||
|
created: [],
|
||||||
|
accepted: [],
|
||||||
|
available: [],
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,7 +143,9 @@ class InvitationsService {
|
|||||||
|
|
||||||
/// Get invitations stream for real-time updates
|
/// Get invitations stream for real-time updates
|
||||||
static Stream<InvitationsData> getInvitationsStream() {
|
static Stream<InvitationsData> getInvitationsStream() {
|
||||||
return CacheService.getStream<InvitationsData>(CacheService.invitationsAllKey);
|
return CacheService.getStream<InvitationsData>(
|
||||||
|
CacheService.invitationsAllKey,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if polling is active
|
/// Check if polling is active
|
||||||
@ -161,7 +196,9 @@ class InvitationsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> createInvitation(Map<String, dynamic> invitationData) async {
|
static Future<Map<String, dynamic>> createInvitation(
|
||||||
|
Map<String, dynamic> invitationData,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.post(
|
final response = await HttpService.post(
|
||||||
ApiConstants.createInvitationEndpoint,
|
ApiConstants.createInvitationEndpoint,
|
||||||
@ -209,7 +246,9 @@ class InvitationsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> getInvitationDetails(int invitationId) async {
|
static Future<Map<String, dynamic>> getInvitationDetails(
|
||||||
|
int invitationId,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.get(
|
final response = await HttpService.get(
|
||||||
'${ApiConstants.invitationsEndpoint}/get?id=$invitationId',
|
'${ApiConstants.invitationsEndpoint}/get?id=$invitationId',
|
||||||
@ -218,15 +257,15 @@ class InvitationsService {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final responseData = jsonDecode(response.body);
|
final responseData = jsonDecode(response.body);
|
||||||
if (responseData['status'] == true) {
|
if (responseData['status'] == true) {
|
||||||
final invitationDetails = InvitationDetails.fromJson(responseData['data']);
|
final invitationDetails = InvitationDetails.fromJson(
|
||||||
return {
|
responseData['data'],
|
||||||
'success': true,
|
);
|
||||||
'data': invitationDetails,
|
return {'success': true, 'data': invitationDetails};
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': responseData['message'] ?? 'Failed to get invitation details',
|
'message':
|
||||||
|
responseData['message'] ?? 'Failed to get invitation details',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (response.statusCode == 401) {
|
} else if (response.statusCode == 401) {
|
||||||
|
|||||||
@ -165,6 +165,61 @@ class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> updateUser({
|
||||||
|
String? displayName,
|
||||||
|
String? username,
|
||||||
|
String? avatar,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final Map<String, dynamic> updateData = {};
|
||||||
|
if (displayName != null) updateData['displayName'] = displayName;
|
||||||
|
if (username != null) updateData['username'] = username;
|
||||||
|
if (avatar != null) updateData['avatar'] = avatar;
|
||||||
|
|
||||||
|
final response = await HttpService.post(ApiConstants.updateUserEndpoint, updateData);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (responseData['status'] == true) {
|
||||||
|
// Update cache with new user data
|
||||||
|
CacheService.setCached(CacheService.userProfileKey, responseData['data']);
|
||||||
|
// Also save to AuthService for backward compatibility
|
||||||
|
await AuthService.saveUserData(responseData['data']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': true,
|
||||||
|
'message': responseData['message'] ?? 'Profile updated successfully',
|
||||||
|
'data': responseData['data'],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': responseData['message'] ?? 'Failed to update profile',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
|
await AuthService.handleAuthenticationError();
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Session expired. Please login again.',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': responseData['message'] ?? 'Server error (${response.statusCode})',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating user: $e');
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Network error. Please check your connection.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> createUser({
|
static Future<Map<String, dynamic>> createUser({
|
||||||
required String email,
|
required String email,
|
||||||
required String password,
|
required String password,
|
||||||
|
|||||||
@ -25,6 +25,8 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- assets/app_info.txt
|
||||||
fonts:
|
fonts:
|
||||||
- family: Blaka
|
- family: Blaka
|
||||||
fonts:
|
fonts:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user